gin 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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