kookaburra 0.18.3 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -1
- data/README.markdown +114 -135
- data/VERSION +1 -1
- data/kookaburra.gemspec +8 -10
- data/lib/kookaburra.rb +22 -31
- data/lib/kookaburra/api_driver.rb +95 -11
- data/lib/kookaburra/given_driver.rb +32 -16
- data/lib/kookaburra/json_api_driver.rb +43 -70
- data/lib/kookaburra/{test_data.rb → mental_model.rb} +15 -9
- data/lib/kookaburra/null_browser.rb +4 -0
- data/lib/kookaburra/test_helpers.rb +2 -4
- data/lib/kookaburra/ui_driver.rb +15 -11
- data/lib/kookaburra/ui_driver/ui_component.rb +22 -5
- data/spec/integration/test_a_rack_application_spec.rb +171 -137
- data/spec/kookaburra/api_driver_spec.rb +126 -0
- data/spec/kookaburra/json_api_driver_spec.rb +85 -30
- data/spec/kookaburra/{test_data_spec.rb → mental_model_spec.rb} +6 -6
- data/spec/kookaburra/ui_driver_spec.rb +5 -3
- data/spec/kookaburra_spec.rb +9 -41
- metadata +9 -11
- data/lib/kookaburra/rack_driver.rb +0 -109
- data/lib/kookaburra/utils/active_record_shared_connection.rb +0 -14
- data/spec/kookaburra/rack_driver_spec.rb +0 -42
data/lib/kookaburra.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'kookaburra/exceptions'
|
2
|
-
require 'kookaburra/
|
2
|
+
require 'kookaburra/mental_model'
|
3
3
|
require 'kookaburra/given_driver'
|
4
4
|
require 'kookaburra/ui_driver'
|
5
5
|
|
@@ -29,33 +29,33 @@ class Kookaburra
|
|
29
29
|
# Returns a new Kookaburra instance that wires together your application's
|
30
30
|
# APIDriver, GivenDriver, and UIDriver.
|
31
31
|
#
|
32
|
-
# @option options [Class] :api_driver_class Your application's
|
33
|
-
# subclass of {Kookaburra::APIDriver}. At the moment, only the
|
34
|
-
# {Kookaburra::JsonApiDriver} is implemented
|
35
32
|
# @option options [Class] :given_driver_class Your
|
36
33
|
# application's subclass of {Kookaburra::GivenDriver}
|
37
34
|
# @option options [Class] :ui_driver_class Your application's
|
38
35
|
# subclass of {Kookaburra::UIDriver}
|
39
|
-
# @option options [Capybara::Session] :browser
|
36
|
+
# @option options [Capybara::Session] :browser The browser driver
|
40
37
|
# that Kookaburra will interact with to run the tests.
|
41
|
-
# @option options [
|
42
|
-
# against.
|
38
|
+
# @option options [String] :app_host The URL of your running application
|
39
|
+
# server against which the tests will be run (e.g.
|
40
|
+
# "http://my_app.example.com:12345")
|
41
|
+
# @option options [Proc] :server_error_detection A proc that will receive the
|
42
|
+
# object passed in to the :browser option as an argument and must return
|
43
|
+
# `true` if the server responded with an unexpected error or `false` if it
|
44
|
+
# did not.
|
43
45
|
def initialize(options = {})
|
44
|
-
@api_driver_class = options[:api_driver_class]
|
45
46
|
@given_driver_class = options[:given_driver_class]
|
46
47
|
@ui_driver_class = options[:ui_driver_class]
|
47
48
|
@browser = options[:browser]
|
48
|
-
@
|
49
|
+
@app_host = options[:app_host]
|
49
50
|
@server_error_detection = options[:server_error_detection]
|
50
51
|
end
|
51
52
|
|
52
53
|
# Returns an instance of your GivenDriver class configured to share test
|
53
|
-
# fixture data with the UIDriver
|
54
|
-
# communicate with your application
|
54
|
+
# fixture data with the UIDriver
|
55
55
|
#
|
56
56
|
# @return [Kookaburra::GivenDriver]
|
57
57
|
def given
|
58
|
-
given_driver_class.new(:
|
58
|
+
given_driver_class.new(:mental_model => mental_model, :app_host => @app_host)
|
59
59
|
end
|
60
60
|
|
61
61
|
# Returns an instance of your UIDriver class configured to share test fixture
|
@@ -64,18 +64,19 @@ class Kookaburra
|
|
64
64
|
#
|
65
65
|
# @return [Kookaburra::UIDriver]
|
66
66
|
def ui
|
67
|
-
ui_driver_class.new(:
|
67
|
+
ui_driver_class.new(:mental_model => mental_model,
|
68
68
|
:browser => browser,
|
69
|
+
:app_host => @app_host,
|
69
70
|
:server_error_detection => @server_error_detection)
|
70
71
|
end
|
71
72
|
|
72
|
-
# Returns a frozen copy of the specified
|
73
|
+
# Returns a frozen copy of the specified {MentalModel::Collection}.
|
73
74
|
# However, this is neither a deep copy nor a deep freeze, so it is possible
|
74
75
|
# that you could modify data outside of your GivenDriver or UIDriver. Just
|
75
76
|
# don't do that. Trust me.
|
76
77
|
#
|
77
|
-
# This access is provided so that you can reference the current
|
78
|
-
# within your test implementation
|
78
|
+
# This access is provided so that you can reference the current mental model
|
79
|
+
# within your test implementation and make assertions about the state
|
79
80
|
# of your application's interface.
|
80
81
|
#
|
81
82
|
# @example
|
@@ -83,28 +84,18 @@ class Kookaburra
|
|
83
84
|
# ui.create_a_new_widget(:bar)
|
84
85
|
# ui.widget_list.widgets.should == k.get_data(:widgets).slice(:foo, :bar)
|
85
86
|
#
|
86
|
-
# @return [Kookaburra::
|
87
|
+
# @return [Kookaburra::MentalModel::Collection]
|
87
88
|
def get_data(collection_name)
|
88
|
-
|
89
|
+
mental_model.send(collection_name).dup.freeze
|
89
90
|
end
|
90
91
|
|
91
92
|
private
|
92
93
|
|
93
94
|
extend DependencyAccessor
|
94
|
-
dependency_accessor :given_driver_class, :
|
95
|
+
dependency_accessor :given_driver_class, :ui_driver_class
|
95
96
|
|
96
|
-
def
|
97
|
-
|
98
|
-
end
|
99
|
-
|
100
|
-
def application_driver
|
101
|
-
unless @rack_app.nil?
|
102
|
-
RackDriver.new(@rack_app)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def test_data
|
107
|
-
@test_data ||= TestData.new
|
97
|
+
def mental_model
|
98
|
+
@mental_model ||= MentalModel.new
|
108
99
|
end
|
109
100
|
|
110
101
|
def browser
|
@@ -1,16 +1,100 @@
|
|
1
|
+
require 'kookaburra/exceptions'
|
2
|
+
require 'delegate'
|
3
|
+
require 'patron'
|
4
|
+
|
1
5
|
class Kookaburra
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# {Kookaburra::JsonApiDriver} should be factored up into this class.
|
7
|
-
#
|
8
|
-
# @abstract Subclass and provide an API client implementation
|
9
|
-
class APIDriver
|
10
|
-
# Returns a new APIDriver.
|
6
|
+
class APIDriver < SimpleDelegator
|
7
|
+
# Wraps `:http_client` in a `SimpleDelegator` that causes request methods to
|
8
|
+
# either return the response body or raise an exception on an unexpected
|
9
|
+
# response status code.
|
11
10
|
#
|
12
|
-
# @
|
13
|
-
|
11
|
+
# @option options [String] :app_host The root URL of your running
|
12
|
+
# application (e.g. "http://my_app.example.com:12345")
|
13
|
+
# @option options [Patron::Session] :http_client (Patron::Session.new) The
|
14
|
+
# object responsible for actually making HTTP calls to your application.
|
15
|
+
def initialize(options = {})
|
16
|
+
http_client = options[:http_client] || Patron::Session.new
|
17
|
+
http_client.base_url = options[:app_host] if options.has_key?(:app_host)
|
18
|
+
super(http_client)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Makes a POST request via the `:http_client`
|
22
|
+
#
|
23
|
+
# @param [String] path The path to request (e.g. "/foo")
|
24
|
+
# @param [Hash, String] data The post data. If a Hash is provided, it will
|
25
|
+
# be converted to an 'application/x-www-form-urlencoded' post body.
|
26
|
+
# @option options [Integer] :expected_response_status (201) The HTTP status
|
27
|
+
# code that you expect the server to respond with.
|
28
|
+
# @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
|
29
|
+
# response does not match the `:expected_response_status`
|
30
|
+
def post(path, data, options = {})
|
31
|
+
request(:post, path, options, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Makes a PUT request via the `:http_client`
|
35
|
+
#
|
36
|
+
# @param [String] path The path to request (e.g. "/foo")
|
37
|
+
# @param [Hash, String] data The post data. If a Hash is provided, it will
|
38
|
+
# be converted to an 'application/x-www-form-urlencoded' post body.
|
39
|
+
# @option options [Integer] :expected_response_status (201) The HTTP status
|
40
|
+
# code that you expect the server to respond with.
|
41
|
+
# @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
|
42
|
+
# response does not match the `:expected_response_status`
|
43
|
+
def put(path, data, options = {})
|
44
|
+
request(:put, path, options, data)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Makes a GET request via the `:http_client`
|
48
|
+
#
|
49
|
+
# @param [String] path The path to request (e.g. "/foo")
|
50
|
+
# @option options [Integer] :expected_response_status (201) The HTTP status
|
51
|
+
# code that you expect the server to respond with.
|
52
|
+
# @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
|
53
|
+
# response does not match the `:expected_response_status`
|
54
|
+
def get(path, options = {})
|
55
|
+
request(:get, path, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Makes a DELETE request via the `:http_client`
|
59
|
+
#
|
60
|
+
# @param [String] path The path to request (e.g. "/foo")
|
61
|
+
# @option options [Integer] :expected_response_status (201) The HTTP status
|
62
|
+
# code that you expect the server to respond with.
|
63
|
+
# @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
|
64
|
+
# response does not match the `:expected_response_status`
|
65
|
+
def delete(path, options = {})
|
66
|
+
request(:delete, path, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def request(type, path, options = {}, data = nil)
|
72
|
+
# don't send a data argument if it's not passed in, because some methods
|
73
|
+
# on target object may not have the proper arity (i.e. #get and #delete).
|
74
|
+
args = [type, path, data, options].compact
|
75
|
+
response = __getobj__.send(*args)
|
76
|
+
|
77
|
+
check_response_status!(type, response, options)
|
78
|
+
response.body
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_response_status!(request_type, response, options)
|
82
|
+
verb, default_status = verb_map[request_type]
|
83
|
+
expected_status = options[:expected_response_status] || default_status
|
84
|
+
unless expected_status == response.status
|
85
|
+
raise UnexpectedResponse, "#{verb} to #{response.url} responded with " \
|
86
|
+
+ "#{response.status} status, not #{expected_status} as expected\n\n" \
|
87
|
+
+ response.body
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def verb_map
|
92
|
+
{
|
93
|
+
:get => ['GET', 200],
|
94
|
+
:post => ['POST', 201],
|
95
|
+
:put => ['PUT', 200],
|
96
|
+
:delete => ['DELETE', 200]
|
97
|
+
}
|
14
98
|
end
|
15
99
|
end
|
16
100
|
end
|
@@ -7,12 +7,17 @@ class Kookaburra
|
|
7
7
|
# comprised of several distinct API calls as well as access to Kookaburra's
|
8
8
|
# test data store.
|
9
9
|
#
|
10
|
-
# @abstract Subclass and implement your Given DSL
|
10
|
+
# @abstract Subclass and implement your Given DSL. You must also provide an
|
11
|
+
# implementation of #api that returns an instance of your APIDriver.
|
11
12
|
#
|
12
13
|
# @example GivenDriver subclass
|
13
14
|
# module MyApp
|
14
15
|
# module Kookaburra
|
15
16
|
# class GivenDriver < ::Kookaburra::GivenDriver
|
17
|
+
# def api
|
18
|
+
# @api ||= APIDriver.new(:app_host => initialization_options[:app_host])
|
19
|
+
# end
|
20
|
+
#
|
16
21
|
# def a_widget(name, attributes = {})
|
17
22
|
# # Set up the data that will be passed to the API by merging any
|
18
23
|
# # passed attributes into the default data.
|
@@ -21,9 +26,9 @@ class Kookaburra
|
|
21
26
|
# # Call the API method and get the resulting response as Ruby data.
|
22
27
|
# result = api.create_widget(data)
|
23
28
|
#
|
24
|
-
# # Store the resulting widget data in the
|
29
|
+
# # Store the resulting widget data in the MentalModel object, so that
|
25
30
|
# # it can be referenced in other operations.
|
26
|
-
#
|
31
|
+
# mental_model.widgets[name] = result
|
27
32
|
# end
|
28
33
|
# end
|
29
34
|
# end
|
@@ -34,27 +39,38 @@ class Kookaburra
|
|
34
39
|
# It is unlikely that you would call #initialize yourself; your GivenDriver
|
35
40
|
# object is instantiated for you by {Kookaburra#given}.
|
36
41
|
#
|
37
|
-
# @option options [Kookaburra::
|
38
|
-
#
|
42
|
+
# @option options [Kookaburra::MentalModel] :mental_model the MentalModel
|
43
|
+
# instance used by your tests
|
44
|
+
# @option options [String] :app_host The root URL of your running
|
45
|
+
# application (e.g. "http://my_app.example.com:12345")
|
39
46
|
def initialize(options = {})
|
40
|
-
@
|
41
|
-
@
|
47
|
+
@initialization_options = options
|
48
|
+
@mental_model = options[:mental_model]
|
42
49
|
end
|
43
50
|
|
44
51
|
protected
|
45
52
|
|
46
|
-
#
|
47
|
-
# instance was created with.
|
53
|
+
# Used to access your APIDriver in your own GivenDriver implementation
|
48
54
|
#
|
49
|
-
# @
|
50
|
-
# @return [Kookaburra::
|
51
|
-
|
55
|
+
# @abstract
|
56
|
+
# @return [Kookaburra::APIDriver]
|
57
|
+
# @raise [Kookaburra::ConfigurationError] raised if you do not provide an
|
58
|
+
# implementation.
|
59
|
+
def api
|
60
|
+
raise ConfigurationError, "You must implement #api in your subclass."
|
61
|
+
end
|
52
62
|
|
53
|
-
#
|
54
|
-
#
|
63
|
+
# The full set of options passed in to {#initialize}
|
64
|
+
#
|
65
|
+
# Access is provided so that you can use these when instantiating your
|
66
|
+
# {APIDriver} in your {#api} implementation.
|
67
|
+
attr_reader :initialization_options
|
68
|
+
|
69
|
+
# A reference to the {Kookaburra::MentalModel} object that this GivenDriver
|
70
|
+
# instance was created with.
|
55
71
|
#
|
56
72
|
# @attribute [r]
|
57
|
-
# @return [Kookaburra::
|
58
|
-
dependency_accessor :
|
73
|
+
# @return [Kookaburra::MentalModel]
|
74
|
+
dependency_accessor :mental_model
|
59
75
|
end
|
60
76
|
end
|
@@ -1,90 +1,63 @@
|
|
1
|
-
require 'kookaburra/dependency_accessor'
|
2
|
-
require 'kookaburra/rack_driver'
|
3
1
|
require 'kookaburra/api_driver'
|
2
|
+
require 'delegate'
|
4
3
|
require 'active_support/json'
|
5
4
|
|
6
5
|
class Kookaburra
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# JSON <-> Ruby and setting the necessary request headers.
|
6
|
+
# Delegates all methods (by default) to and instance of
|
7
|
+
# {Kookaburra::APIDriver}.
|
10
8
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
def initialize(app_driver)
|
30
|
-
@app_driver = app_driver
|
9
|
+
# Expects the application's API to accept and respond with JSON formatted
|
10
|
+
# data. All methods will decode the response body using
|
11
|
+
# `ActiveSupport::JSON.decode`. Methods that take input data ({#post} and
|
12
|
+
# {#put}) will encode the post data using `ActiveSupport::JSON.encode`.
|
13
|
+
class JsonApiDriver < SimpleDelegator
|
14
|
+
#
|
15
|
+
# Sets both the "Content-Type" and "Accept" headers to "application/json".
|
16
|
+
#
|
17
|
+
# @option options [Kookaburra::APIDriver] :api_driver (Kookaburra::APIDriver.new)
|
18
|
+
# The APIDriver instance to be delegated to. Changing this is probably
|
19
|
+
# only useful for testing.
|
20
|
+
def initialize(options = {})
|
21
|
+
api_driver = options[:api_driver] || APIDriver.new(:app_host => options[:app_host])
|
22
|
+
api_driver.headers.merge!(
|
23
|
+
'Content-Type' => 'application/json',
|
24
|
+
'Accept' => 'application/json'
|
25
|
+
)
|
26
|
+
super(api_driver)
|
31
27
|
end
|
32
28
|
|
33
|
-
|
29
|
+
def post(path, data, *args)
|
30
|
+
request(:post, path, data, *args)
|
31
|
+
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
#
|
38
|
-
# @example
|
39
|
-
# def create_widget(data)
|
40
|
-
# authorize('api_user', 'api_password')
|
41
|
-
# post '/widgets', data
|
42
|
-
# end
|
43
|
-
#
|
44
|
-
# @param [String] username
|
45
|
-
# @param [String] password
|
46
|
-
def authorize(username, password)
|
47
|
-
@app_driver.authorize(username, password)
|
33
|
+
def put(path, data, *args)
|
34
|
+
request(:put, path, data, *args)
|
48
35
|
end
|
49
36
|
|
50
|
-
|
51
|
-
|
52
|
-
# @param [String] path The path portion of the URI that should be requested
|
53
|
-
# @param [Hash] data The data that should be translated into a JSON post
|
54
|
-
# body for the request.
|
55
|
-
#
|
56
|
-
# @return [Hash] The decoded response from the server
|
57
|
-
#
|
58
|
-
# @example
|
59
|
-
# # in your JsonApiDriver subclass
|
60
|
-
# def create_widget(data)
|
61
|
-
# post '/widgets', data
|
62
|
-
# end
|
63
|
-
#
|
64
|
-
# # in a method within your GivenDriver
|
65
|
-
# api.create_widget(:name => 'Foo')
|
66
|
-
# #=> {:id => 1, :name => 'Foo', :description => ''}
|
67
|
-
def post(path, data)
|
68
|
-
do_json_request(:post, path, data)
|
37
|
+
def get(path, *args)
|
38
|
+
request(:get, path, nil, *args)
|
69
39
|
end
|
70
40
|
|
71
|
-
def
|
72
|
-
|
41
|
+
def delete(path, *args)
|
42
|
+
request(:delete, path, nil, *args)
|
73
43
|
end
|
74
44
|
|
75
|
-
|
76
|
-
|
45
|
+
private
|
46
|
+
|
47
|
+
def request(type, path, data = nil, *args)
|
48
|
+
# don't want to send data to methods that don't accept it
|
49
|
+
args = [path, encode(data), args].flatten.compact
|
50
|
+
output = __getobj__.send(type, *args)
|
51
|
+
|
52
|
+
decode(output)
|
77
53
|
end
|
78
54
|
|
79
|
-
|
55
|
+
def encode(data)
|
56
|
+
ActiveSupport::JSON.encode(data) unless data.nil?
|
57
|
+
end
|
80
58
|
|
81
|
-
def
|
82
|
-
|
83
|
-
'Content-Type' => 'application/json',
|
84
|
-
'Accept' => 'application/json'
|
85
|
-
}
|
86
|
-
response = @app_driver.send(method.to_sym, path, J.encode(data), json_request_headers)
|
87
|
-
J.decode(response)
|
59
|
+
def decode(data)
|
60
|
+
ActiveSupport::JSON.decode(data)
|
88
61
|
end
|
89
62
|
end
|
90
63
|
end
|
@@ -1,37 +1,43 @@
|
|
1
1
|
require 'delegate'
|
2
2
|
|
3
3
|
class Kookaburra
|
4
|
-
# Each instance of {Kookaburra} has its own instance of
|
4
|
+
# Each instance of {Kookaburra} has its own instance of MentalModel. This object
|
5
5
|
# is used to maintain a shared understanding of the application state between
|
6
6
|
# your {GivenDriver} and your {UIDriver}. You can access the various test data
|
7
7
|
# collections in your test implementations via {Kookaburra#get_data}.
|
8
|
-
|
8
|
+
#
|
9
|
+
# The mental model is not intended to represent a copy of all of the data
|
10
|
+
# within your application. Rather it is meant to represent the mental image of
|
11
|
+
# the data that a user of your application might have while working with your
|
12
|
+
# system. Certainly you *can* store whatever you want in it, but thinking
|
13
|
+
# about it in these terms can help you design better, more robust tests.
|
14
|
+
class MentalModel
|
9
15
|
def initialize
|
10
16
|
@data = {}
|
11
17
|
end
|
12
18
|
|
13
|
-
#
|
14
|
-
# returning either a new or existing {
|
19
|
+
# MentalModel instances will respond to any message that has an arity of 0 by
|
20
|
+
# returning either a new or existing {MentalModel::Collection} having the name
|
15
21
|
# of the method.
|
16
22
|
def method_missing(name, *args)
|
17
23
|
return super unless args.empty?
|
18
24
|
@data[name] ||= Collection.new(name)
|
19
25
|
end
|
20
26
|
|
21
|
-
#
|
27
|
+
# MentalModel instances respond to everything.
|
22
28
|
#
|
23
29
|
# @see #method_missing
|
24
30
|
def respond_to?
|
25
31
|
true
|
26
32
|
end
|
27
33
|
|
28
|
-
# A
|
34
|
+
# A MentalModel::Collection behaves much like a `Hash` object, with the
|
29
35
|
# exception that it will raise an {UnknownKeyError} rather than return nil
|
30
36
|
# if you attempt to access a key that has not been set. The exception
|
31
37
|
# attempts to provide a more helpful error message.
|
32
38
|
#
|
33
39
|
# @example
|
34
|
-
# widgets = Kookaburra::
|
40
|
+
# widgets = Kookaburra::MentalModel::Collection.new('widgets')
|
35
41
|
#
|
36
42
|
# widgets[:foo] = :a_foo
|
37
43
|
#
|
@@ -39,14 +45,14 @@ class Kookaburra
|
|
39
45
|
# #=> :a_foo
|
40
46
|
#
|
41
47
|
# # Raises an UnknownKeyError
|
42
|
-
#
|
48
|
+
# mental_model.widgets[:bar]
|
43
49
|
class Collection < SimpleDelegator
|
44
50
|
# @param [String] name The name of the collection. Used to provide
|
45
51
|
# helpful error messages when unknown keys are accessed.
|
46
52
|
def initialize(name)
|
47
53
|
@name = name
|
48
54
|
data = Hash.new do |hash, key|
|
49
|
-
raise UnknownKeyError, "Can't find
|
55
|
+
raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
|
50
56
|
end
|
51
57
|
super(data)
|
52
58
|
end
|