gin 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ ##
2
+ # Read-Write lock pair for accessing data that is mostly read-bound.
3
+ # Reading is done without locking until a write operation is started.
4
+ #
5
+ # lock = Gin::RWLock.new
6
+ # lock.write_sync{ write_to_the_object }
7
+ # value = lock.read_sync{ read_from_the_object }
8
+ #
9
+ # The RWLock is built to work primarily in Thread-pool type environments and its
10
+ # effectiveness is much less for Thread-spawn models.
11
+ #
12
+ # RWLock also shows increased performance in GIL-less Ruby implementations such
13
+ # as Rubinius 2.x.
14
+ #
15
+ # Using write_sync from inside a read_sync block is safe, but the inverse isn't:
16
+ #
17
+ # lock = Gin::RWLock.new
18
+ #
19
+ # # This is OK.
20
+ # lock.read_sync do
21
+ # get_value || lock.write_sync{ update_value }
22
+ # end
23
+ #
24
+ # # This is NOT OK and will raise a ThreadError.
25
+ # # It's also not necessary because read sync-ing is inferred
26
+ # # during write syncs.
27
+ # lock.write_sync do
28
+ # update_value
29
+ # lock.read_sync{ get_value }
30
+ # end
31
+
32
+ class Gin::RWLock
33
+
34
+ class WriteTimeout < StandardError; end
35
+
36
+ TIMEOUT_MSG = "Took too long to lock all config mutexes. \
37
+ Try increasing the value of Config#write_timeout."
38
+
39
+ # The amount of time to wait for writer threads to get all the read locks.
40
+ attr_accessor :write_timeout
41
+
42
+
43
+ def initialize write_timeout=nil
44
+ @wmutex = Mutex.new
45
+ @write_timeout = write_timeout || 0.05
46
+ @mutex_id = :"rwlock_#{self.object_id}"
47
+ @mutex_owned_id = :"#{@mutex_id}_owned"
48
+ end
49
+
50
+
51
+ def write_sync
52
+ lock_mutexes = []
53
+ relock_curr = false
54
+ was_locked = Thread.current[@mutex_owned_id]
55
+
56
+ curr_mutex = read_mutex
57
+
58
+ write_mutex.lock unless was_locked
59
+ Thread.current[@mutex_owned_id] = true
60
+
61
+ # Protect against same-thread deadlocks
62
+ if curr_mutex && curr_mutex.locked?
63
+ relock_curr = curr_mutex.unlock rescue false
64
+ end
65
+
66
+ start = Time.now
67
+
68
+ Thread.list.each do |t|
69
+ mutex = t[@mutex_id]
70
+ next if !mutex || !relock_curr && t == Thread.current
71
+ until mutex.try_lock
72
+ raise WriteTimeout, TIMEOUT_MSG if Time.now - start > @write_timeout
73
+ end
74
+ lock_mutexes << mutex
75
+ end
76
+
77
+ yield
78
+ ensure
79
+ lock_mutexes.each(&:unlock)
80
+ curr_mutex.try_lock if relock_curr
81
+ unless was_locked
82
+ Thread.current[@mutex_owned_id] = false
83
+ write_mutex.unlock
84
+ end
85
+ end
86
+
87
+
88
+ def read_sync
89
+ was_locked = read_mutex.locked?
90
+ read_mutex.lock unless was_locked
91
+ yield
92
+ ensure
93
+ read_mutex.unlock if was_locked
94
+ end
95
+
96
+
97
+ private
98
+
99
+
100
+ def write_mutex
101
+ @wmutex
102
+ end
103
+
104
+
105
+ def read_mutex
106
+ return Thread.current[@mutex_id] if Thread.current[@mutex_id]
107
+ @wmutex.synchronize{ Thread.current[@mutex_id] = Mutex.new }
108
+ end
109
+ end
data/lib/gin/test.rb ADDED
@@ -0,0 +1,702 @@
1
+ require 'time'
2
+
3
+ module Gin::Test; end
4
+
5
+ ##
6
+ # Helper assertion methods for tests.
7
+ # To contextualize tests to a specific app, use the
8
+ # automatically generated module assigned to your app's class:
9
+ #
10
+ # class MyCtrlTest < Test::Unit::TestCase
11
+ # include MyApp::TestHelper # Sets App for mock requests.
12
+ # controller MyHomeController # Sets default controller to use.
13
+ #
14
+ # def test_home
15
+ # get :home
16
+ # assert_response :success
17
+ # end
18
+ # end
19
+
20
+ module Gin::Test::Assertions
21
+
22
+ ##
23
+ # Asserts the response status code and headers.
24
+ # Takes an integer (status code) or Symbol as the expected value:
25
+ # :success:: 2XX status codes
26
+ # :redirect:: 301-303, 307-308 status codes
27
+ # :forbidden:: 403 status code
28
+ # :unauthorized:: 401 status code
29
+ # :not_found:: 404 status code
30
+
31
+ def assert_response expected, msg=nil
32
+ status = rack_response[0]
33
+ case expected
34
+ when :success
35
+ assert((200..299).include?(status),
36
+ msg || "Status expected to be in range 200..299 but was #{status.inspect}")
37
+ when :redirect
38
+ assert [301,302,303,307,308].include?(status),
39
+ msg || "Status expected to be in range 301..303 or 307..308 but was #{status.inspect}"
40
+ when :unauthorized
41
+ assert 401 == status,
42
+ msg || "Status expected to be 401 but was #{status.inspect}"
43
+ when :forbidden
44
+ assert 403 == status,
45
+ msg || "Status expected to be 403 but was #{status.inspect}"
46
+ when :not_found
47
+ assert 404 == status,
48
+ msg || "Status expected to be 404 but was #{status.inspect}"
49
+ when :error
50
+ assert((500..599).include?(status),
51
+ msg || "Status expected to be in range 500..599 but was #{status.inspect}")
52
+ else
53
+ assert expected == status,
54
+ msg || "Status expected to be #{expected.inspect} but was #{status.inspect}"
55
+ end
56
+ end
57
+
58
+
59
+ ##
60
+ # Checks for data points in the response body.
61
+ # Looks at the response Content-Type to parse.
62
+ # Supports JSON, BSON, XML, PLIST, and HTML.
63
+ #
64
+ # If value is a Class, Range, or Regex, does a match.
65
+ # Options supported are:
66
+ # :count:: Integer - Number of occurences of the data point.
67
+ # :value:: Object - The expected value of the data point.
68
+ # :selector:: Symbol - type of selector to use: :css, :xpath, or :rb_path
69
+ #
70
+ # If value is a Class, Range, or Regex, does a match.
71
+ #
72
+ # # Use CSS3 for HTML
73
+ # assert_select '.address[domestic=Yes]'
74
+ #
75
+ # # Use XPath for XML data
76
+ # assert_select './/address[@domestic=Yes]'
77
+ #
78
+ # # Use ruby-path for JSON, BSON, and PList
79
+ # assert_select '**/address/domestic=YES/../value'
80
+
81
+ def assert_select key_or_path, opts={}, msg=nil
82
+ value = opts[:value]
83
+ data = parsed_body
84
+ val_msg = " with value #{value.inspect}" if !value.nil?
85
+ count = 0
86
+
87
+ selector = opts[:selector] ||
88
+ case data
89
+ when Array, Hash then :rb_path
90
+ when Nokogiri::HTML::Document then :xpath
91
+ when Nokogiri::XML::Document then :css
92
+ end
93
+
94
+ case selector
95
+ when :rb_path
96
+ use_lib 'path', 'ruby-path'
97
+ data.find_data(key_or_path) do |p,k,pa|
98
+ count += 1 if value.nil? || value === p[k]
99
+ break unless opts[:count]
100
+ end
101
+
102
+ when :css
103
+ data.css(key_or_path).each do |node|
104
+ count += 1 if value.nil? || value === node.text
105
+ break unless opts[:count]
106
+ end
107
+
108
+ when :xpath
109
+ data.xpath(key_or_path).each do |node|
110
+ count += 1 if value.nil? || value === node.text
111
+ break unless opts[:count]
112
+ end
113
+
114
+ else
115
+ raise "Unknown selector #{selector.inspect} for #{data.class}"
116
+ end
117
+
118
+ if opts[:count]
119
+ assert opts[:count] == count,
120
+ msg || "Expected #{opts[:count]} items matching '#{key_or_path}'#{val_msg} but found #{count}"
121
+ else
122
+ assert((count > 0),
123
+ msg || "Expected at least one item matching '#{key_or_path}'#{val_msg} but found none")
124
+ end
125
+ end
126
+
127
+
128
+ ##
129
+ # Uses ruby-path to check for data points in the response body.
130
+ #
131
+ # Options supported are:
132
+ # :count:: Integer - Number of occurences of the data point.
133
+ # :value:: Object - The expected value of the data point.
134
+ #
135
+ # If value is a Class, Range, or Regex, does a match.
136
+ # Use for JSON, BSON, and PList data.
137
+ # assert_select '**/address/domestic=YES/../value'
138
+
139
+ def assert_data path, opts={}, msg=nil
140
+ assert_select path, opts.merge(selector: :rb_path), msg
141
+ end
142
+
143
+
144
+ ##
145
+ # Uses CSS selectors to check for data points in the response body.
146
+ #
147
+ # Options supported are:
148
+ # :count:: Integer - Number of occurences of the data point.
149
+ # :value:: Object - The expected value of the data point.
150
+ #
151
+ # If value is a Class, Range, or Regex, does a match.
152
+ # Use for XML or HTML.
153
+ # assert_select '.address[domestic=Yes]'
154
+
155
+ def assert_css path, opts={}, msg=nil
156
+ assert_select path, opts.merge(selector: :css), msg
157
+ end
158
+
159
+
160
+ ##
161
+ # Uses XPath selectors to check for data points in the response body.
162
+ #
163
+ # Options supported are:
164
+ # :count:: Integer - Number of occurences of the data point.
165
+ # :value:: Object - The expected value of the data point.
166
+ #
167
+ # If value is a Class, Range, or Regex, does a match.
168
+ # Use for XML or HTML.
169
+ # assert_select './/address[@domestic=Yes]'
170
+
171
+ def assert_xpath path, opts={}, msg=nil
172
+ assert_select path, opts.merge(selector: :xpath), msg
173
+ end
174
+
175
+
176
+ ##
177
+ # Checks that the given Cookie is set with the expected values.
178
+ # Options supported:
179
+ # :secure:: Boolean - SSL cookies only
180
+ # :http_only:: Boolean - HTTP only cookie
181
+ # :domain:: String - Domain on which the cookie is used
182
+ # :expires_at:: Time - Date and time of cookie expiration
183
+ # :path:: String - Path cookie applies to
184
+ # :value:: Object - The value of the cookie
185
+
186
+ def assert_cookie name, opts={}, msg=nil
187
+ opts ||= {}
188
+ cookie = response_cookies[name]
189
+
190
+ assert cookie, msg || "Expected cookie #{name.inspect} but it doesn't exist"
191
+
192
+ opts.each do |k,v|
193
+ next if v == cookie[k]
194
+ err_msg = msg || "Expected cookie #{k} to be #{v.inspect} but was #{cookie[k].inspect}"
195
+
196
+ raise MiniTest::Assertion, err_msg.to_s
197
+ end
198
+ end
199
+
200
+
201
+ ##
202
+ # Checks that a rendered view name or path matches the one given.
203
+
204
+ def assert_view view, msg=nil
205
+ path = @controller.template_path(view)
206
+ expected = @app.template_files(path).first
207
+ assert templates.include?(expected),
208
+ msg || "Expected view `#{path}' in:\n #{templates.join("\n ")}"
209
+ end
210
+
211
+
212
+ ##
213
+ # Checks that a specific layout was use to render the response.
214
+
215
+ def assert_layout layout, msg=nil
216
+ path = @controller.template_path(layout, true)
217
+ expected = @app.template_files(path).first
218
+ assert templates.include?(expected),
219
+ msg || "Expected layout `#{path}' in:\n #{templates.join("\n ")}"
220
+ end
221
+
222
+
223
+ ##
224
+ # Checks that the response is a redirect to a given path or url.
225
+ # assert_redirect "/path/to/thing"
226
+ # assert_redirect "http://example.com"
227
+ # assert_redirect 302, "/path/to/thing"
228
+
229
+ def assert_redirect url, *args
230
+ status = args.shift if Integer === args[0]
231
+ location = rack_response[1]['Location']
232
+
233
+ msg = args.pop ||
234
+ "Expected redirect to #{url.inspect} but was #{location.inspect}"
235
+
236
+ raise MiniTest::Assertion, msg unless url == location
237
+ assert_response(status || :redirect)
238
+ end
239
+
240
+
241
+ ##
242
+ # Checks that the given route is valid and points to the expected
243
+ # controller and action.
244
+
245
+ def assert_route verb, path, exp_ctrl, exp_action, msg=nil
246
+ ctrl, action, = app.router.resources_for(verb, path)
247
+ expected = "#{exp_ctrl}##{exp_action}"
248
+ real = "#{ctrl}##{action}"
249
+ real_msg = ctrl && action ? "got #{real}" : "doesn't exist"
250
+
251
+ assert expected == real,
252
+ msg || "`#{verb.to_s.upcase} #{path}' should map to #{expected} but #{real_msg}"
253
+ end
254
+ end
255
+
256
+
257
+ ##
258
+ # Helper methods for tests. To contextualize tests to a specific app, use the
259
+ # automatically generated module assigned to your app's class:
260
+ #
261
+ # class MyCtrlTest < Test::Unit::TestCase
262
+ # include MyApp::TestHelper # Sets App for mock requests.
263
+ # controller MyHomeController # Sets default controller to use.
264
+ #
265
+ # def test_home
266
+ # get :home
267
+ # assert_response :success
268
+ # end
269
+ # end
270
+ #
271
+ # All requests are full stack, meaning any in-app middleware will be run as
272
+ # a part of a request. The goal is to test controllers in the context of the
273
+ # whole app, and easily do integration-level tests as well.
274
+
275
+ module Gin::Test::Helpers
276
+
277
+ include Gin::Test::Assertions
278
+
279
+ def self.setup_klass subclass # :nodoc:
280
+ return if subclass.respond_to?(:app_klass)
281
+
282
+ subclass.instance_eval do
283
+ def app_klass klass=nil
284
+ @app_klass = klass if klass
285
+ defined?(@app_klass) && @app_klass
286
+ end
287
+
288
+
289
+ ##
290
+ # Sets the default controller to use when making requests
291
+ # for all tests in the given class.
292
+ # class MyCtrlTest < Test::Unit::TestCase
293
+ # include MyApp::TestHelper
294
+ # controller MyCtrl
295
+ # end
296
+
297
+ def controller ctrl_klass=nil
298
+ @default_controller = ctrl_klass if ctrl_klass
299
+ defined?(@default_controller) && @default_controller
300
+ end
301
+ end
302
+ end
303
+
304
+
305
+ def use_lib lib, gemname=nil # :nodoc:
306
+ require lib
307
+ rescue LoadError => e
308
+ raise unless e.message == "cannot load such file -- #{lib}"
309
+ gemname ||= lib
310
+ $stderr.puts "You need the `#{gemname}' gem to access some of the features \
311
+ you are trying to use.
312
+ Run the following command and try again: gem install #{gemname}"
313
+ exit 1
314
+ end
315
+
316
+
317
+ def correct_302_redirect?
318
+ self.class.correct_302_redirect?
319
+ end
320
+
321
+
322
+ ##
323
+ # The App instance being used for the requests.
324
+
325
+ def app
326
+ @app ||= self.class.app_klass.new
327
+ end
328
+
329
+
330
+ ##
331
+ # The Rack env for the next mock request.
332
+
333
+ def env
334
+ @env ||= {'rack.input' => ""}
335
+ end
336
+
337
+
338
+ ##
339
+ # The standard Rack response array.
340
+
341
+ def rack_response
342
+ @rack_response ||= [nil,{},[]]
343
+ end
344
+
345
+
346
+ ##
347
+ # The Gin::Controller instance used by the last mock request.
348
+
349
+ def controller
350
+ defined?(@controller) && @controller
351
+ end
352
+
353
+
354
+ ##
355
+ # The Gin::Request instance on the controller used by the last mock request.
356
+
357
+ def request
358
+ controller && controller.request
359
+ end
360
+
361
+
362
+ ##
363
+ # The Gin::Response instance on the controller used by the last mock request.
364
+
365
+ def response
366
+ controller && controller.response
367
+ end
368
+
369
+
370
+ ##
371
+ # Array of template file paths used to render the response body.
372
+
373
+ def templates
374
+ @templates ||= []
375
+ end
376
+
377
+
378
+ ##
379
+ # Make a GET request.
380
+ # get FooController, :show, :id => 123
381
+ #
382
+ # # With default_controller set to FooController
383
+ # get :show, :id => 123
384
+ #
385
+ # # Default named route
386
+ # get :show_foo, :id => 123
387
+ #
388
+ # # Request with headers
389
+ # get :show_foo, {:id => 123}, 'Cookie' => 'value'
390
+ # get :show_foo, {}, 'Cookie' => 'value'
391
+
392
+ def get *args
393
+ make_request :get, *args
394
+ end
395
+
396
+
397
+ ##
398
+ # Make a POST request. See 'get' method for usage.
399
+
400
+ def post *args
401
+ make_request :post, *args
402
+ end
403
+
404
+
405
+ ##
406
+ # Make a PUT request. See 'get' method for usage.
407
+
408
+ def put *args
409
+ make_request :put, *args
410
+ end
411
+
412
+
413
+ ##
414
+ # Make a PATCH request. See 'get' method for usage.
415
+
416
+ def patch *args
417
+ make_request :patch, *args
418
+ end
419
+
420
+
421
+ ##
422
+ # Make a DELETE request. See 'get' method for usage.
423
+
424
+ def delete *args
425
+ make_request :delete, *args
426
+ end
427
+
428
+
429
+ ##
430
+ # Make a HEAD request. See 'get' method for usage.
431
+
432
+ def head *args
433
+ make_request :head, *args
434
+ end
435
+
436
+
437
+ ##
438
+ # Make a OPTIONS request. See 'get' method for usage.
439
+
440
+ def options *args
441
+ make_request :options, *args
442
+ end
443
+
444
+
445
+ ##
446
+ # Make a mock request to the given http verb and path,
447
+ # controller+action, or named route.
448
+ #
449
+ # make_request :get, FooController, :show, :id => 123
450
+ #
451
+ # # With default_controller set to FooController
452
+ # make_request :get, :show, :id => 123
453
+ #
454
+ # # Default named route
455
+ # make_request :get, :show_foo, :id => 123
456
+ #
457
+ # # Request with headers
458
+ # make_request :get, :show_foo, {:id => 123}, 'Cookie' => 'value'
459
+ # make_request :get, :show_foo, {}, 'Cookie' => 'value'
460
+
461
+ def make_request verb, *args
462
+ headers = (Hash === args[-2] && Hash === args[-1]) ? args.pop : {}
463
+ path, query = path_to(*args).split("?")
464
+
465
+ env['HTTP_COOKIE'] = @set_cookies.map{|k,v| "#{k}=#{v}"}.join("; ") if
466
+ defined?(@set_cookies) && @set_cookies && !@set_cookies.empty?
467
+
468
+ env['REQUEST_METHOD'] = verb.to_s.upcase
469
+ env['QUERY_STRING'] = query
470
+ env['PATH_INFO'] = path
471
+ env.merge! headers
472
+
473
+ @rack_response = app.call(env)
474
+ @controller = env[Gin::Constants::GIN_CTRL]
475
+ @templates = env[Gin::Constants::GIN_TEMPLATES]
476
+
477
+ @env = nil
478
+ @body = nil
479
+ @parsed_body = nil
480
+ @set_cookies = nil
481
+
482
+ cookies.each{|n, c| set_cookie(n, c[:value]) }
483
+
484
+ @rack_response
485
+ end
486
+
487
+
488
+ ##
489
+ # Sets a cookie for the next mock request.
490
+ # set_cookie "mycookie", "FOO"
491
+
492
+ def set_cookie name, value
493
+ @set_cookies ||= {}
494
+ @set_cookies[name] = value
495
+ end
496
+
497
+
498
+ COOKIE_MATCH = /\A([^(),\/<>@;:\\\"\[\]?={}\s]+)(?:=([^;]*))?\Z/ # :nodoc:
499
+
500
+ ##
501
+ # Cookies assigned to the response. Will not show expired cookies,
502
+ # but cookies will otherwise persist across multiple requests in the
503
+ # same test case.
504
+ # cookies['session']
505
+ # #=> {:value => "foo", :expires => <#Time>}
506
+
507
+ def cookies
508
+ return @cookies if defined?(@cookie_key) &&
509
+ @cookie_key == rack_response[1]['Set-Cookie']
510
+
511
+ @response_cookies = {}
512
+
513
+ Array(rack_response[1]['Set-Cookie']).each do |set_cookie_value|
514
+ args = { }
515
+ params=set_cookie_value.split(/;\s*/)
516
+
517
+ first=true
518
+ params.each do |param|
519
+ result = COOKIE_MATCH.match param
520
+ if !result
521
+ raise "Invalid cookie parameter in cookie '#{set_cookie_value}'"
522
+ end
523
+
524
+ key = result[1].downcase.to_sym
525
+ keyvalue = result[2]
526
+ if first
527
+ args[:name] = result[1]
528
+ args[:value] = CGI.unescape(keyvalue.to_s)
529
+ first = false
530
+ else
531
+ case key
532
+ when :expires
533
+ begin
534
+ args[:expires_at] = Time.parse keyvalue
535
+ rescue ArgumentError
536
+ raise unless $!.message == "time out of range"
537
+ args[:expires_at] = Time.at(0x7FFFFFFF)
538
+ end
539
+ when *[:domain, :path]
540
+ args[key] = keyvalue
541
+ when :secure
542
+ args[:secure] = true
543
+ when :httponly
544
+ args[:http_only] = true
545
+ else
546
+ raise "Unknown cookie parameter '#{key}'"
547
+ end
548
+ end
549
+ end
550
+
551
+ @response_cookies[args[:name]] = args
552
+ end
553
+
554
+ @cookie_key = rack_response[1]['Set-Cookie']
555
+ (@cookies ||= {}).merge!(@response_cookies)
556
+ @cookies
557
+ end
558
+
559
+
560
+ ##
561
+ # Cookies assigned by the last response.
562
+
563
+ def response_cookies
564
+ cookies unless defined?(@response_cookies)
565
+ @response_cookies ||= {}
566
+ end
567
+
568
+
569
+ ##
570
+ # The read String body of the response.
571
+
572
+ def body
573
+ return @body if defined?(@body) && @body
574
+ @body = ""
575
+ rack_response[2].each{|str| @body << str }
576
+ @body
577
+ end
578
+
579
+
580
+ ##
581
+ # The data representing the parsed String body
582
+ # of the response, according to the Content-Type.
583
+ #
584
+ # Supports JSON, BSON, XML, PLIST, and HTML.
585
+ # Returns plain Ruby objects for JSON, BSON, and PLIST.
586
+ # Returns a Nokogiri document object for XML and HTML.
587
+
588
+ def parsed_body
589
+ return @parsed_body if defined?(@parsed_body) && @parsed_body
590
+ ct = rack_response[1]['Content-Type']
591
+
592
+ @parsed_body =
593
+ case ct
594
+ when /[\/+]json/i
595
+ use_lib 'json'
596
+ JSON.parse(body)
597
+
598
+ when /[\/+]bson/i
599
+ use_lib 'bson'
600
+ BSON.deserialize(body)
601
+
602
+ when /[\/+]plist/i
603
+ use_lib 'plist'
604
+ Plist.parse_xml(body)
605
+
606
+ when /[\/+]xml/i
607
+ use_lib 'nokogiri'
608
+ Nokogiri::XML(body)
609
+
610
+ when /[\/+]html/i
611
+ use_lib 'nokogiri'
612
+ Nokogiri::HTML(body)
613
+
614
+ else
615
+ raise "No parser available for content-type #{ct.inspect}"
616
+ end
617
+ end
618
+
619
+
620
+ ##
621
+ # The body stream as returned by the Rack response Array.
622
+ # Responds to #each.
623
+
624
+ def stream
625
+ rack_response[2]
626
+ end
627
+
628
+
629
+ ##
630
+ # Sets the default controller to use when making requests.
631
+ # Best used in a test setup context.
632
+ #
633
+ # def setup
634
+ # default_controller HomeController
635
+ # end
636
+
637
+ def default_controller ctrl_klass=nil
638
+ @default_controller = ctrl_klass if ctrl_klass
639
+ defined?(@default_controller) && @default_controller || self.class.controller
640
+ end
641
+
642
+
643
+ ##
644
+ # Build a path to the given controller and action or route name,
645
+ # with any expected params. If no controller is specified and the default
646
+ # controller responds to the symbol given, uses the default controller for
647
+ # path lookup.
648
+ #
649
+ # path_to FooController, :show, :id => 123
650
+ # #=> "/foo/123"
651
+ #
652
+ # # With default_controller set to FooController
653
+ # path_to :show, :id => 123
654
+ # #=> "/foo/123"
655
+ #
656
+ # # Default named route
657
+ # path_to :show_foo, :id => 123
658
+ # #=> "/foo/123"
659
+
660
+ def path_to *args
661
+ return "#{args[0]}#{"?" << Gin.build_query(args[1]) if args[1]}" if String === args[0]
662
+
663
+ args.unshift(@default_controller) if
664
+ Symbol === args[0] && defined?(@default_controller) &&
665
+ @default_controller && @default_controller.actions.include?(args[0])
666
+
667
+ app.router.path_to(*args)
668
+ end
669
+ end
670
+
671
+
672
+ class Gin::App # :nodoc:
673
+ class << self
674
+ alias old_inherited inherited
675
+ end
676
+
677
+ def self.inherited subclass
678
+ old_inherited subclass
679
+ subclass.define_test_helper
680
+ end
681
+
682
+
683
+ def self.define_test_helper
684
+ return const_get(:TestHelper) if const_defined?(:TestHelper)
685
+ class_eval <<-STR
686
+ module TestHelper
687
+ include Gin::Test::Helpers
688
+
689
+ def self.included subclass
690
+ Gin::Test::Helpers.setup_klass(subclass)
691
+ subclass.app_klass #{self}
692
+ end
693
+ end
694
+ STR
695
+
696
+ const_get :TestHelper
697
+ end
698
+ end
699
+
700
+ ObjectSpace.each_object(Class) do |klass|
701
+ klass.define_test_helper if klass < Gin::App
702
+ end