redinger-rr 0.10.3

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.
Files changed (133) hide show
  1. data/CHANGES +221 -0
  2. data/README.rdoc +343 -0
  3. data/Rakefile +88 -0
  4. data/VERSION.yml +4 -0
  5. data/lib/rr.rb +88 -0
  6. data/lib/rr/adapters/rr_methods.rb +122 -0
  7. data/lib/rr/adapters/rspec.rb +59 -0
  8. data/lib/rr/adapters/test_unit.rb +29 -0
  9. data/lib/rr/double.rb +152 -0
  10. data/lib/rr/double_definitions/child_double_definition_creator.rb +27 -0
  11. data/lib/rr/double_definitions/double_definition.rb +348 -0
  12. data/lib/rr/double_definitions/double_definition_creator.rb +167 -0
  13. data/lib/rr/double_definitions/double_definition_creator_proxy.rb +37 -0
  14. data/lib/rr/double_definitions/strategies/implementation/implementation_strategy.rb +15 -0
  15. data/lib/rr/double_definitions/strategies/implementation/proxy.rb +62 -0
  16. data/lib/rr/double_definitions/strategies/implementation/reimplementation.rb +14 -0
  17. data/lib/rr/double_definitions/strategies/implementation/strongly_typed_reimplementation.rb +17 -0
  18. data/lib/rr/double_definitions/strategies/scope/instance.rb +15 -0
  19. data/lib/rr/double_definitions/strategies/scope/instance_of_class.rb +50 -0
  20. data/lib/rr/double_definitions/strategies/scope/scope_strategy.rb +15 -0
  21. data/lib/rr/double_definitions/strategies/strategy.rb +70 -0
  22. data/lib/rr/double_definitions/strategies/verification/dont_allow.rb +34 -0
  23. data/lib/rr/double_definitions/strategies/verification/mock.rb +44 -0
  24. data/lib/rr/double_definitions/strategies/verification/stub.rb +45 -0
  25. data/lib/rr/double_definitions/strategies/verification/verification_strategy.rb +15 -0
  26. data/lib/rr/double_injection.rb +180 -0
  27. data/lib/rr/double_matches.rb +51 -0
  28. data/lib/rr/errors/argument_equality_error.rb +6 -0
  29. data/lib/rr/errors/double_definition_error.rb +6 -0
  30. data/lib/rr/errors/double_not_found_error.rb +6 -0
  31. data/lib/rr/errors/double_order_error.rb +6 -0
  32. data/lib/rr/errors/rr_error.rb +20 -0
  33. data/lib/rr/errors/spy_verification_errors/double_injection_not_found_error.rb +8 -0
  34. data/lib/rr/errors/spy_verification_errors/invocation_count_error.rb +8 -0
  35. data/lib/rr/errors/spy_verification_errors/spy_verification_error.rb +8 -0
  36. data/lib/rr/errors/subject_does_not_implement_method_error.rb +6 -0
  37. data/lib/rr/errors/subject_has_different_arity_error.rb +6 -0
  38. data/lib/rr/errors/times_called_error.rb +6 -0
  39. data/lib/rr/expectations/any_argument_expectation.rb +21 -0
  40. data/lib/rr/expectations/argument_equality_expectation.rb +41 -0
  41. data/lib/rr/expectations/times_called_expectation.rb +57 -0
  42. data/lib/rr/hash_with_object_id_key.rb +44 -0
  43. data/lib/rr/method_dispatches/base_method_dispatch.rb +108 -0
  44. data/lib/rr/method_dispatches/method_dispatch.rb +61 -0
  45. data/lib/rr/method_dispatches/method_missing_dispatch.rb +49 -0
  46. data/lib/rr/proc_from_block.rb +7 -0
  47. data/lib/rr/recorded_calls.rb +103 -0
  48. data/lib/rr/space.rb +123 -0
  49. data/lib/rr/spy_verification.rb +48 -0
  50. data/lib/rr/spy_verification_proxy.rb +18 -0
  51. data/lib/rr/times_called_matchers/any_times_matcher.rb +18 -0
  52. data/lib/rr/times_called_matchers/at_least_matcher.rb +15 -0
  53. data/lib/rr/times_called_matchers/at_most_matcher.rb +23 -0
  54. data/lib/rr/times_called_matchers/integer_matcher.rb +19 -0
  55. data/lib/rr/times_called_matchers/non_terminal.rb +27 -0
  56. data/lib/rr/times_called_matchers/proc_matcher.rb +11 -0
  57. data/lib/rr/times_called_matchers/range_matcher.rb +21 -0
  58. data/lib/rr/times_called_matchers/terminal.rb +20 -0
  59. data/lib/rr/times_called_matchers/times_called_matcher.rb +44 -0
  60. data/lib/rr/wildcard_matchers.rb +158 -0
  61. data/lib/rr/wildcard_matchers/anything.rb +18 -0
  62. data/lib/rr/wildcard_matchers/boolean.rb +23 -0
  63. data/lib/rr/wildcard_matchers/duck_type.rb +32 -0
  64. data/lib/rr/wildcard_matchers/hash_including.rb +29 -0
  65. data/lib/rr/wildcard_matchers/is_a.rb +25 -0
  66. data/lib/rr/wildcard_matchers/numeric.rb +13 -0
  67. data/lib/rr/wildcard_matchers/range.rb +7 -0
  68. data/lib/rr/wildcard_matchers/regexp.rb +7 -0
  69. data/lib/rr/wildcard_matchers/satisfy.rb +26 -0
  70. data/spec/core_spec_suite.rb +19 -0
  71. data/spec/environment_fixture_setup.rb +7 -0
  72. data/spec/high_level_spec.rb +398 -0
  73. data/spec/proc_from_block_spec.rb +14 -0
  74. data/spec/rr/adapters/rr_methods_argument_matcher_spec.rb +67 -0
  75. data/spec/rr/adapters/rr_methods_creator_spec.rb +149 -0
  76. data/spec/rr/adapters/rr_methods_space_spec.rb +115 -0
  77. data/spec/rr/adapters/rr_methods_spec_helper.rb +11 -0
  78. data/spec/rr/adapters/rr_methods_times_matcher_spec.rb +17 -0
  79. data/spec/rr/double_definitions/child_double_definition_creator_spec.rb +112 -0
  80. data/spec/rr/double_definitions/double_definition_creator_proxy_spec.rb +155 -0
  81. data/spec/rr/double_definitions/double_definition_creator_spec.rb +502 -0
  82. data/spec/rr/double_definitions/double_definition_spec.rb +1165 -0
  83. data/spec/rr/double_injection/double_injection_spec.rb +339 -0
  84. data/spec/rr/double_injection/double_injection_verify_spec.rb +29 -0
  85. data/spec/rr/double_spec.rb +352 -0
  86. data/spec/rr/errors/rr_error_spec.rb +67 -0
  87. data/spec/rr/expectations/any_argument_expectation_spec.rb +47 -0
  88. data/spec/rr/expectations/anything_argument_equality_expectation_spec.rb +14 -0
  89. data/spec/rr/expectations/argument_equality_expectation_spec.rb +135 -0
  90. data/spec/rr/expectations/boolean_argument_equality_expectation_spec.rb +34 -0
  91. data/spec/rr/expectations/hash_including_argument_equality_expectation_spec.rb +82 -0
  92. data/spec/rr/expectations/hash_including_spec.rb +17 -0
  93. data/spec/rr/expectations/satisfy_argument_equality_expectation_spec.rb +59 -0
  94. data/spec/rr/expectations/satisfy_spec.rb +14 -0
  95. data/spec/rr/expectations/times_called_expectation/times_called_expectation_any_times_spec.rb +46 -0
  96. data/spec/rr/expectations/times_called_expectation/times_called_expectation_at_least_spec.rb +69 -0
  97. data/spec/rr/expectations/times_called_expectation/times_called_expectation_at_most_spec.rb +71 -0
  98. data/spec/rr/expectations/times_called_expectation/times_called_expectation_helper.rb +23 -0
  99. data/spec/rr/expectations/times_called_expectation/times_called_expectation_integer_spec.rb +104 -0
  100. data/spec/rr/expectations/times_called_expectation/times_called_expectation_proc_spec.rb +81 -0
  101. data/spec/rr/expectations/times_called_expectation/times_called_expectation_range_spec.rb +83 -0
  102. data/spec/rr/expectations/times_called_expectation/times_called_expectation_spec.rb +38 -0
  103. data/spec/rr/rspec/invocation_matcher_spec.rb +279 -0
  104. data/spec/rr/rspec/rspec_adapter_spec.rb +66 -0
  105. data/spec/rr/rspec/rspec_backtrace_tweaking_spec.rb +31 -0
  106. data/spec/rr/rspec/rspec_backtrace_tweaking_spec_fixture.rb +11 -0
  107. data/spec/rr/rspec/rspec_usage_spec.rb +86 -0
  108. data/spec/rr/space/hash_with_object_id_key_spec.rb +88 -0
  109. data/spec/rr/space/space_spec.rb +550 -0
  110. data/spec/rr/test_unit/test_helper.rb +7 -0
  111. data/spec/rr/test_unit/test_unit_backtrace_test.rb +36 -0
  112. data/spec/rr/test_unit/test_unit_integration_test.rb +57 -0
  113. data/spec/rr/times_called_matchers/any_times_matcher_spec.rb +47 -0
  114. data/spec/rr/times_called_matchers/at_least_matcher_spec.rb +55 -0
  115. data/spec/rr/times_called_matchers/at_most_matcher_spec.rb +70 -0
  116. data/spec/rr/times_called_matchers/integer_matcher_spec.rb +70 -0
  117. data/spec/rr/times_called_matchers/proc_matcher_spec.rb +55 -0
  118. data/spec/rr/times_called_matchers/range_matcher_spec.rb +76 -0
  119. data/spec/rr/times_called_matchers/times_called_matcher_spec.rb +118 -0
  120. data/spec/rr/wildcard_matchers/anything_spec.rb +24 -0
  121. data/spec/rr/wildcard_matchers/boolean_spec.rb +36 -0
  122. data/spec/rr/wildcard_matchers/duck_type_spec.rb +52 -0
  123. data/spec/rr/wildcard_matchers/is_a_spec.rb +32 -0
  124. data/spec/rr/wildcard_matchers/numeric_spec.rb +32 -0
  125. data/spec/rr/wildcard_matchers/range_spec.rb +35 -0
  126. data/spec/rr/wildcard_matchers/regexp_spec.rb +43 -0
  127. data/spec/rr_spec.rb +28 -0
  128. data/spec/rspec_spec_suite.rb +17 -0
  129. data/spec/spec_helper.rb +109 -0
  130. data/spec/spec_suite.rb +31 -0
  131. data/spec/spy_verification_spec.rb +129 -0
  132. data/spec/test_unit_spec_suite.rb +21 -0
  133. metadata +193 -0
@@ -0,0 +1,48 @@
1
+ module RR
2
+ class SpyVerification
3
+ def initialize(subject, method_name, args)
4
+ @subject = subject
5
+ @method_name = method_name.to_sym
6
+ set_argument_expectation_for_args(args)
7
+ @ordered = false
8
+ once
9
+ end
10
+
11
+ attr_reader :argument_expectation, :method_name, :times_matcher
12
+ attr_accessor :subject
13
+
14
+ include RR::DoubleDefinitions::DoubleDefinition::TimesDefinitionConstructionMethods
15
+ include RR::DoubleDefinitions::DoubleDefinition::ArgumentDefinitionConstructionMethods
16
+
17
+ def ordered
18
+ @ordered = true
19
+ self
20
+ end
21
+
22
+ def ordered?
23
+ @ordered
24
+ end
25
+
26
+ def call
27
+ (error = RR.recorded_calls.match_error(self)) && raise(error)
28
+ end
29
+
30
+ def to_proc
31
+ lambda do
32
+ call
33
+ end
34
+ end
35
+
36
+ protected
37
+ attr_writer :times_matcher
38
+
39
+ def set_argument_expectation_for_args(args)
40
+ # with_no_args and with actually set @argument_expectation
41
+ args.empty? ? with_no_args : with(*args)
42
+ end
43
+
44
+ def install_method_callback(return_value_block)
45
+ # Do nothing. This is to support DefinitionConstructionMethods
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ module RR
2
+ class SpyVerificationProxy
3
+ instance_methods.each do |m|
4
+ unless m =~ /^_/ || m.to_s == 'object_id' || m.to_s == "instance_eval" || m.to_s == "instance_exec" || m.to_s == 'respond_to?'
5
+ alias_method "__blank_slated_#{m}", m
6
+ undef_method m
7
+ end
8
+ end
9
+
10
+ def initialize(subject)
11
+ @subject = subject
12
+ end
13
+
14
+ def method_missing(method_name, *args, &block)
15
+ SpyVerification.new(@subject, method_name, args)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module RR
2
+ module TimesCalledMatchers #:nodoc:
3
+ class AnyTimesMatcher < TimesCalledMatcher
4
+ include NonTerminal
5
+
6
+ def initialize
7
+ end
8
+
9
+ def matches?(times_called)
10
+ true
11
+ end
12
+
13
+ def expected_times_message
14
+ "any number of times"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module RR
2
+ module TimesCalledMatchers #:nodoc:
3
+ class AtLeastMatcher < TimesCalledMatcher
4
+ include NonTerminal
5
+
6
+ def matches?(times_called)
7
+ times_called >= @times
8
+ end
9
+
10
+ def expected_times_message
11
+ "at least #{@times.inspect} times"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module RR
2
+ module TimesCalledMatchers #:nodoc:
3
+ class AtMostMatcher < TimesCalledMatcher
4
+ include Terminal
5
+
6
+ def possible_match?(times_called)
7
+ times_called <= @times
8
+ end
9
+
10
+ def matches?(times_called)
11
+ times_called <= @times
12
+ end
13
+
14
+ def attempt?(times_called)
15
+ times_called < @times
16
+ end
17
+
18
+ def expected_times_message
19
+ "at most #{@times.inspect} times"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module RR
2
+ module TimesCalledMatchers #:nodoc:
3
+ class IntegerMatcher < TimesCalledMatcher
4
+ include Terminal
5
+
6
+ def possible_match?(times_called)
7
+ times_called <= @times
8
+ end
9
+
10
+ def matches?(times_called)
11
+ times_called == @times
12
+ end
13
+
14
+ def attempt?(times_called)
15
+ times_called < @times
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module RR
2
+ module TimesCalledMatchers
3
+ # Including this module marks the TimesCalledMatcher as NonTerminal.
4
+ # Being NonTerminal means the Double will not "terminate" even when
5
+ # called infinite times.
6
+ #
7
+ # The Double that uses a NonTerminal TimesCalledMatcher will
8
+ # continue using the Double when passed the matching arguments.
9
+ # This is done by the attempt? always returning true.
10
+ #
11
+ # This is in opposition to Terminal TimesCalledMatchers, where
12
+ # attempt? will eventually return false.
13
+ module NonTerminal #:nodoc:
14
+ def terminal?
15
+ false
16
+ end
17
+
18
+ def possible_match?(times_called)
19
+ true
20
+ end
21
+
22
+ def attempt?(times_called)
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module RR
2
+ module TimesCalledMatchers
3
+ class ProcMatcher < TimesCalledMatcher #:nodoc:
4
+ include NonTerminal
5
+
6
+ def matches?(times_called)
7
+ @times.call(times_called)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module RR
2
+ module TimesCalledMatchers
3
+ class RangeMatcher < TimesCalledMatcher #:nodoc:
4
+ include Terminal
5
+
6
+ def possible_match?(times_called)
7
+ return true if times_called < @times.begin
8
+ return true if @times.include?(times_called)
9
+ return false
10
+ end
11
+
12
+ def matches?(times_called)
13
+ @times.include?(times_called)
14
+ end
15
+
16
+ def attempt?(times_called)
17
+ possible_match?(times_called)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module RR
2
+ module TimesCalledMatchers
3
+ # Including this module marks the TimesCalledMatcher as Terminal.
4
+ # Being Terminal the Double will "terminate" when times called is
5
+ # finite.
6
+ #
7
+ # The Double that uses a Terminal TimesCalledMatcher will
8
+ # eventually be passed over to the next Double when passed
9
+ # the matching arguments enough times. This is done by the attempt?
10
+ # method returning false when executed a finite number of times.
11
+ #
12
+ # This is in opposition to NonTerminal TimesCalledMatchers, where
13
+ # attempt? will always return true.
14
+ module Terminal #:nodoc:
15
+ def terminal?
16
+ true
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ module RR
2
+ module TimesCalledMatchers
3
+ class TimesCalledMatcher #:nodoc:
4
+ class << self
5
+ def create(value)
6
+ return value if value.is_a?(TimesCalledMatcher)
7
+ return IntegerMatcher.new(value) if value.is_a?(Integer)
8
+ return RangeMatcher.new(value) if value.is_a?(Range )
9
+ return ProcMatcher.new(value) if value.is_a?(Proc)
10
+ raise ArgumentError, "There is no TimesCalledMatcher for #{value.inspect}."
11
+ end
12
+ end
13
+
14
+ attr_reader :times
15
+
16
+ def initialize(times)
17
+ @times = times
18
+ end
19
+
20
+ def matches?(times_called)
21
+ end
22
+
23
+ def attempt?(times_called)
24
+ end
25
+
26
+ def error_message(times_called)
27
+ "Called #{times_called.inspect} #{pluralized_time(times_called)}.\nExpected #{expected_times_message}."
28
+ end
29
+
30
+ def ==(other)
31
+ self.class == other.class && self.times == other.times
32
+ end
33
+
34
+ def expected_times_message
35
+ "#{@times.inspect} times"
36
+ end
37
+
38
+ protected
39
+ def pluralized_time(times_called)
40
+ (times_called == 1) ? "time" : "times"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,158 @@
1
+ =begin rdoc
2
+
3
+ = Writing your own custom wildcard matchers.
4
+ Writing new wildcard matchers is not too difficult. If you've ever written
5
+ a custom expectation in RSpec, the implementation is very similar.
6
+
7
+ As an example, let's say that you want a matcher that will match any number
8
+ divisible by a certain integer. In use, it might look like this:
9
+
10
+ # Will pass if BananaGrabber#bunch_bananas is called with an integer
11
+ # divisible by 5.
12
+
13
+ mock(BananaGrabber).bunch_bananas(divisible_by(5))
14
+
15
+ To implement this, we need a class RR::WildcardMatchers::DivisibleBy with
16
+ these instance methods:
17
+
18
+ * ==(other)
19
+ * eql?(other) (usually aliased to #==)
20
+ * inspect
21
+ * wildcard_match?(other)
22
+
23
+ and optionally, a sensible initialize method. Let's look at each of these.
24
+
25
+ === .initialize
26
+
27
+ Most custom wildcard matchers will want to define initialize to store
28
+ some information about just what should be matched. DivisibleBy#initialize
29
+ might look like this:
30
+
31
+ class RR::WildcardMatchers::DivisibleBy
32
+ def initialize(divisor)
33
+ @expected_divisor = divisor
34
+ end
35
+ end
36
+
37
+ === #==(other)
38
+ DivisibleBy#==(other) should return true if other is a wildcard matcher that
39
+ matches the same things as self, so a natural way to write DivisibleBy#== is:
40
+
41
+
42
+ class RR::WildcardMatchers::DivisibleBy
43
+ def ==(other)
44
+ # Ensure that other is actually a DivisibleBy
45
+ return false unless other.is_a?(self.class)
46
+
47
+ # Does other expect to match the same divisor we do?
48
+ self.expected_divisor = other.expected_divisor
49
+ end
50
+ end
51
+
52
+ Note that this implementation of #== assumes that we've also declared
53
+ attr_reader :expected_divisor
54
+
55
+ === #inspect
56
+
57
+ Technically we don't have to declare DivisibleBy#inspect, since inspect is
58
+ defined for every object already. But putting a helpful message in inspect
59
+ will make test failures much clearer, and it only takes about two seconds to
60
+ write it, so let's be nice and do so:
61
+
62
+ class RR::WildcardMatchers::DivisibleBy
63
+ def inspect
64
+ "integer divisible by #{expected.divisor}"
65
+ end
66
+ end
67
+
68
+ Now if we run the example from above:
69
+
70
+ mock(BananaGrabber).bunch_bananas(divisible_by(5))
71
+
72
+ and it fails, we get a helpful message saying
73
+
74
+ bunch_bananas(integer divisible by 5)
75
+ Called 0 times.
76
+ Expected 1 times.
77
+
78
+ === #wildcard_matches?(other)
79
+
80
+ wildcard_matches? is the method that actually checks the argument against the
81
+ expectation. It should return true if other is considered to match,
82
+ false otherwise. In the case of DivisibleBy, wildcard_matches? reads:
83
+
84
+ class RR::WildcardMatchers::DivisibleBy
85
+ def wildcard_matches?(other)
86
+ # If other isn't a number, how can it be divisible by anything?
87
+ return false unless other.is_a?(Numeric)
88
+
89
+ # If other is in fact divisible by expected_divisor, then
90
+ # other modulo expected_divisor should be 0.
91
+
92
+ other % expected_divisor == 0
93
+ end
94
+ end
95
+
96
+ === A finishing touch: wrapping it neatly
97
+
98
+ We could stop here if we were willing to resign ourselves to using
99
+ DivisibleBy this way:
100
+
101
+ mock(BananaGrabber).bunch_bananas(DivisibleBy.new(5))
102
+
103
+ But that's less expressive than the original:
104
+
105
+ mock(BananaGrabber).bunch_bananas(divisible_by(5))
106
+
107
+ To be able to use the convenient divisible_by matcher rather than the uglier
108
+ DivisibleBy.new version, re-open the module RR::Adapters::RRMethods and
109
+ define divisible_by there as a simple wrapper around DivisibleBy.new:
110
+
111
+ module RR::Adapters::RRMethods
112
+ def divisible_by(expected_divisor)
113
+ RR::WildcardMatchers::DivisibleBy.new(expected_divisor)
114
+ end
115
+ end
116
+
117
+ == Recap
118
+
119
+ Here's all the code for DivisibleBy in one place for easy reference:
120
+
121
+ class RR::WildcardMatchers::DivisibleBy
122
+ def initialize(divisor)
123
+ @expected_divisor = divisor
124
+ end
125
+
126
+ def ==(other)
127
+ # Ensure that other is actually a DivisibleBy
128
+ return false unless other.is_a?(self.class)
129
+
130
+ # Does other expect to match the same divisor we do?
131
+ self.expected_divisor = other.expected_divisor
132
+ end
133
+
134
+ def inspect
135
+ "integer divisible by #{expected.divisor}"
136
+ end
137
+
138
+ def wildcard_matches?(other)
139
+ # If other isn't a number, how can it be divisible by anything?
140
+ return false unless other.is_a?(Numeric)
141
+
142
+ # If other is in fact divisible by expected_divisor, then
143
+ # other modulo expected_divisor should be 0.
144
+
145
+ other % expected_divisor == 0
146
+ end
147
+ end
148
+
149
+ module RR::Adapters::RRMethods
150
+ def divisible_by(expected_divisor)
151
+ RR::WildcardMatchers::DivisibleBy.new(expected_divisor)
152
+ end
153
+ end
154
+
155
+ =end
156
+
157
+ module RR::WildcardMatchers
158
+ end
@@ -0,0 +1,18 @@
1
+ module RR
2
+ module WildcardMatchers
3
+ class Anything
4
+ def wildcard_match?(other)
5
+ true
6
+ end
7
+
8
+ def ==(other)
9
+ other.is_a?(self.class)
10
+ end
11
+ alias_method :eql?, :==
12
+
13
+ def inspect
14
+ 'anything'
15
+ end
16
+ end
17
+ end
18
+ end