appmap 0.41.2 → 0.45.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.
- checksums.yaml +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +23 -2
- data/CHANGELOG.md +40 -0
- data/README.md +36 -6
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +4 -2
- data/lib/appmap/class_map.rb +7 -10
- data/lib/appmap/config.rb +98 -28
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +18 -0
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/hook.rb +42 -22
- data/lib/appmap/hook/method.rb +5 -7
- data/lib/appmap/minitest.rb +35 -30
- data/lib/appmap/rails/request_handler.rb +30 -17
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +32 -96
- data/lib/appmap/trace.rb +2 -1
- data/lib/appmap/util.rb +39 -2
- data/lib/appmap/version.rb +2 -2
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +76 -29
- data/spec/class_map_spec.rb +3 -11
- data/spec/config_spec.rb +33 -1
- data/spec/fixtures/hook/custom_instance_method.rb +11 -0
- data/spec/fixtures/hook/method_named_call.rb +11 -0
- data/spec/fixtures/rails5_users_app/Gemfile +7 -3
- data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/create_app +8 -2
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
- data/spec/fixtures/rails6_users_app/Gemfile +5 -4
- data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/create_app +8 -2
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
- data/spec/hook_spec.rb +135 -18
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- data/test/expectations/openssl_test_key_sign1.json +2 -4
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
- data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
- data/test/gem_test.rb +1 -1
- data/test/minitest_test.rb +1 -2
- data/test/rspec_test.rb +1 -20
- metadata +17 -13
- data/exe/appmap +0 -154
- data/spec/rspec_feature_metadata_spec.rb +0 -31
- data/test/cli_test.rb +0 -116
@@ -45,7 +45,6 @@ RSpec.configure do |config|
|
|
45
45
|
# arbitrary gems may also be filtered via:
|
46
46
|
# config.filter_gems_from_backtrace("gem name")
|
47
47
|
|
48
|
-
|
49
48
|
DatabaseCleaner.allow_remote_database_url = true
|
50
49
|
|
51
50
|
config.before(:suite) do
|
@@ -54,13 +53,8 @@ RSpec.configure do |config|
|
|
54
53
|
end
|
55
54
|
|
56
55
|
config.around :each do |example|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
DatabaseCleaner.cleaning do
|
62
|
-
example.run
|
63
|
-
end
|
64
|
-
}.call
|
56
|
+
DatabaseCleaner.cleaning do
|
57
|
+
example.run
|
58
|
+
end
|
65
59
|
end
|
66
60
|
end
|
data/spec/hook_spec.rb
CHANGED
@@ -16,14 +16,7 @@ end
|
|
16
16
|
Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
|
17
17
|
|
18
18
|
describe 'AppMap class Hooking', docker: false do
|
19
|
-
|
20
|
-
def collect_events(tracer)
|
21
|
-
[].tap do |events|
|
22
|
-
while tracer.event?
|
23
|
-
events << tracer.next_event.to_h
|
24
|
-
end
|
25
|
-
end.map(&AppMap::Util.method(:sanitize_event))
|
26
|
-
end
|
19
|
+
include_context 'collect events'
|
27
20
|
|
28
21
|
def invoke_test_file(file, setup: nil, &block)
|
29
22
|
AppMap.configuration = nil
|
@@ -64,13 +57,144 @@ describe 'AppMap class Hooking', docker: false do
|
|
64
57
|
it 'excludes named classes and methods' do
|
65
58
|
load 'spec/fixtures/hook/exclude.rb'
|
66
59
|
package = AppMap::Config::Package.build_from_path('spec/fixtures/hook/exclude.rb')
|
67
|
-
config = AppMap::Config.new('hook_spec', [ package ], %w[ExcludeTest])
|
60
|
+
config = AppMap::Config.new('hook_spec', [ package ], exclude: %w[ExcludeTest])
|
68
61
|
AppMap.configuration = config
|
69
62
|
|
70
63
|
expect(config.never_hook?(ExcludeTest.new.method(:instance_method))).to be_truthy
|
71
64
|
expect(config.never_hook?(ExcludeTest.method(:cls_method))).to be_truthy
|
72
65
|
end
|
73
66
|
|
67
|
+
it "handles an instance method named 'call' without issues" do
|
68
|
+
events_yaml = <<~YAML
|
69
|
+
---
|
70
|
+
- :id: 1
|
71
|
+
:event: :call
|
72
|
+
:defined_class: MethodNamedCall
|
73
|
+
:method_id: call
|
74
|
+
:path: spec/fixtures/hook/method_named_call.rb
|
75
|
+
:lineno: 8
|
76
|
+
:static: false
|
77
|
+
:parameters:
|
78
|
+
- :name: :a
|
79
|
+
:class: Integer
|
80
|
+
:value: '1'
|
81
|
+
:kind: :req
|
82
|
+
- :name: :b
|
83
|
+
:class: Integer
|
84
|
+
:value: '2'
|
85
|
+
:kind: :req
|
86
|
+
- :name: :c
|
87
|
+
:class: Integer
|
88
|
+
:value: '3'
|
89
|
+
:kind: :req
|
90
|
+
- :name: :d
|
91
|
+
:class: Integer
|
92
|
+
:value: '4'
|
93
|
+
:kind: :req
|
94
|
+
- :name: :e
|
95
|
+
:class: Integer
|
96
|
+
:value: '5'
|
97
|
+
:kind: :req
|
98
|
+
:receiver:
|
99
|
+
:class: MethodNamedCall
|
100
|
+
:value: MethodNamedCall
|
101
|
+
- :id: 2
|
102
|
+
:event: :return
|
103
|
+
:parent_id: 1
|
104
|
+
:return_value:
|
105
|
+
:class: String
|
106
|
+
:value: 1 2 3 4 5
|
107
|
+
YAML
|
108
|
+
|
109
|
+
_, tracer = test_hook_behavior 'spec/fixtures/hook/method_named_call.rb', events_yaml do
|
110
|
+
expect(MethodNamedCall.new.call(1, 2, 3, 4, 5)).to eq('1 2 3 4 5')
|
111
|
+
end
|
112
|
+
class_map = AppMap.class_map(tracer.event_methods)
|
113
|
+
expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
|
114
|
+
---
|
115
|
+
- :name: spec/fixtures/hook/method_named_call.rb
|
116
|
+
:type: package
|
117
|
+
:children:
|
118
|
+
- :name: MethodNamedCall
|
119
|
+
:type: class
|
120
|
+
:children:
|
121
|
+
- :name: call
|
122
|
+
:type: function
|
123
|
+
:location: spec/fixtures/hook/method_named_call.rb:8
|
124
|
+
:static: false
|
125
|
+
CLASSMAP
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'can custom hook and label a function' do
|
129
|
+
events_yaml = <<~YAML
|
130
|
+
---
|
131
|
+
- :id: 1
|
132
|
+
:event: :call
|
133
|
+
:defined_class: CustomInstanceMethod
|
134
|
+
:method_id: say_default
|
135
|
+
:path: spec/fixtures/hook/custom_instance_method.rb
|
136
|
+
:lineno: 8
|
137
|
+
:static: false
|
138
|
+
:parameters: []
|
139
|
+
:receiver:
|
140
|
+
:class: CustomInstanceMethod
|
141
|
+
:value: CustomInstance Method fixture
|
142
|
+
- :id: 2
|
143
|
+
:event: :return
|
144
|
+
:parent_id: 1
|
145
|
+
:return_value:
|
146
|
+
:class: String
|
147
|
+
:value: default
|
148
|
+
YAML
|
149
|
+
|
150
|
+
config = AppMap::Config.load({
|
151
|
+
functions: [
|
152
|
+
{
|
153
|
+
package: 'hook_spec',
|
154
|
+
class: 'CustomInstanceMethod',
|
155
|
+
functions: [ :say_default ],
|
156
|
+
labels: ['cowsay']
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}.deep_stringify_keys)
|
160
|
+
|
161
|
+
load 'spec/fixtures/hook/custom_instance_method.rb'
|
162
|
+
hook_cls = CustomInstanceMethod
|
163
|
+
method = hook_cls.instance_method(:say_default)
|
164
|
+
|
165
|
+
require 'appmap/hook/method'
|
166
|
+
hook_method = AppMap::Hook::Method.new(config.package_for_method(method), hook_cls, method)
|
167
|
+
hook_method.activate
|
168
|
+
|
169
|
+
tracer = AppMap.tracing.trace
|
170
|
+
AppMap::Event.reset_id_counter
|
171
|
+
begin
|
172
|
+
expect(CustomInstanceMethod.new.say_default).to eq('default')
|
173
|
+
ensure
|
174
|
+
AppMap.tracing.delete(tracer)
|
175
|
+
end
|
176
|
+
|
177
|
+
events = collect_events(tracer).to_yaml
|
178
|
+
|
179
|
+
expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
|
180
|
+
class_map = AppMap.class_map(tracer.event_methods)
|
181
|
+
expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
|
182
|
+
---
|
183
|
+
- :name: hook_spec
|
184
|
+
:type: package
|
185
|
+
:children:
|
186
|
+
- :name: CustomInstanceMethod
|
187
|
+
:type: class
|
188
|
+
:children:
|
189
|
+
- :name: say_default
|
190
|
+
:type: function
|
191
|
+
:location: spec/fixtures/hook/custom_instance_method.rb:8
|
192
|
+
:static: false
|
193
|
+
:labels:
|
194
|
+
- cowsay
|
195
|
+
CLASSMAP
|
196
|
+
end
|
197
|
+
|
74
198
|
it 'parses labels from comments' do
|
75
199
|
_, tracer = invoke_test_file 'spec/fixtures/hook/labels.rb' do
|
76
200
|
ClassWithLabel.new.fn_with_label
|
@@ -91,9 +215,6 @@ describe 'AppMap class Hooking', docker: false do
|
|
91
215
|
:labels:
|
92
216
|
- has-fn-label
|
93
217
|
:comment: "# @label has-fn-label\\n"
|
94
|
-
:source: |2
|
95
|
-
def fn_with_label
|
96
|
-
end
|
97
218
|
YAML
|
98
219
|
end
|
99
220
|
|
@@ -148,10 +269,6 @@ describe 'AppMap class Hooking', docker: false do
|
|
148
269
|
:type: function
|
149
270
|
:location: spec/fixtures/hook/instance_method.rb:8
|
150
271
|
:static: false
|
151
|
-
:source: |2
|
152
|
-
def say_default
|
153
|
-
'default'
|
154
|
-
end
|
155
272
|
YAML
|
156
273
|
end
|
157
274
|
|
@@ -746,6 +863,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
746
863
|
end
|
747
864
|
secure_compare_event = YAML.load(events).find { |evt| evt[:defined_class] == 'ActiveSupport::SecurityUtils' }
|
748
865
|
secure_compare_event.delete(:lineno)
|
866
|
+
secure_compare_event.delete(:path)
|
749
867
|
|
750
868
|
expect(Diffy::Diff.new(<<~YAML, secure_compare_event.to_yaml).to_s).to eq('')
|
751
869
|
---
|
@@ -753,7 +871,6 @@ describe 'AppMap class Hooking', docker: false do
|
|
753
871
|
:event: :call
|
754
872
|
:defined_class: ActiveSupport::SecurityUtils
|
755
873
|
:method_id: secure_compare
|
756
|
-
:path: lib/active_support/security_utils.rb
|
757
874
|
:static: true
|
758
875
|
:parameters:
|
759
876
|
- :name: :a
|
@@ -837,7 +954,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
837
954
|
entry = cm[1][:children][0][:children][0][:children][0]
|
838
955
|
# Sanity check, make sure we got the right one
|
839
956
|
expect(entry[:name]).to eq('secure_compare')
|
840
|
-
expect(entry[:labels]).to eq(%w[
|
957
|
+
expect(entry[:labels]).to eq(%w[crypto.secure_compare])
|
841
958
|
end
|
842
959
|
end
|
843
960
|
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'diffy'
|
3
|
+
require 'rack'
|
4
|
+
require 'rack/handler/webrick'
|
5
|
+
|
6
|
+
class HelloWorldApp
|
7
|
+
def call(env)
|
8
|
+
req = Rack::Request.new(env)
|
9
|
+
case req.path_info
|
10
|
+
when /hello/
|
11
|
+
[200, {"Content-Type" => "text/html"}, ["Hello World!"]]
|
12
|
+
when /goodbye/
|
13
|
+
[500, {"Content-Type" => "text/html"}, ["Goodbye Cruel World!"]]
|
14
|
+
else
|
15
|
+
[404, {"Content-Type" => "text/html"}, ["I'm Lost!"]]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'Net::HTTP handler' do
|
21
|
+
include_context 'collect events'
|
22
|
+
|
23
|
+
def get_hello(params: nil)
|
24
|
+
http = Net::HTTP.new('localhost', 19292)
|
25
|
+
http.get [ '/hello', params ].compact.join('?')
|
26
|
+
end
|
27
|
+
|
28
|
+
before(:all) do
|
29
|
+
@rack_thread = Thread.new do
|
30
|
+
Rack::Handler::WEBrick.run HelloWorldApp.new, Port: 19292
|
31
|
+
end
|
32
|
+
10.times do
|
33
|
+
sleep 0.1
|
34
|
+
break if get_hello.code.to_i == 200
|
35
|
+
end
|
36
|
+
raise "Web server didn't start" unless get_hello.code.to_i == 200
|
37
|
+
end
|
38
|
+
|
39
|
+
after(:all) do
|
40
|
+
@rack_thread.kill
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_recording
|
44
|
+
AppMap.configuration = configuration
|
45
|
+
AppMap::Hook.new(configuration).enable
|
46
|
+
|
47
|
+
@tracer = AppMap.tracing.trace
|
48
|
+
AppMap::Event.reset_id_counter
|
49
|
+
end
|
50
|
+
|
51
|
+
def record(&block)
|
52
|
+
start_recording
|
53
|
+
begin
|
54
|
+
yield
|
55
|
+
ensure
|
56
|
+
stop_recording
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop_recording
|
61
|
+
AppMap.tracing.delete(@tracer)
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with trace enabled' do
|
65
|
+
let(:configuration) { AppMap::Config.new('record_net_http_spec', []) }
|
66
|
+
|
67
|
+
after do
|
68
|
+
AppMap.configuration = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'GET request' do
|
72
|
+
it 'with a single query parameter' do
|
73
|
+
record do
|
74
|
+
get_hello(params: 'msg=hi')
|
75
|
+
end
|
76
|
+
|
77
|
+
events = collect_events(@tracer).to_yaml
|
78
|
+
expect(Diffy::Diff.new(<<~EVENTS, events).to_s).to eq('')
|
79
|
+
---
|
80
|
+
- :id: 1
|
81
|
+
:event: :call
|
82
|
+
:http_client_request:
|
83
|
+
:request_method: GET
|
84
|
+
:url: http://localhost:19292/hello
|
85
|
+
:headers:
|
86
|
+
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
87
|
+
Accept: "*/*"
|
88
|
+
User-Agent: Ruby
|
89
|
+
Connection: close
|
90
|
+
:message:
|
91
|
+
- :name: msg
|
92
|
+
:class: String
|
93
|
+
:value: hi
|
94
|
+
- :id: 2
|
95
|
+
:event: :return
|
96
|
+
:parent_id: 1
|
97
|
+
:http_client_response:
|
98
|
+
:status_code: 200
|
99
|
+
:headers:
|
100
|
+
Content-Type: text/html
|
101
|
+
Server: WEBrick
|
102
|
+
Date: "<instanceof date>"
|
103
|
+
Content-Length: '12'
|
104
|
+
Connection: close
|
105
|
+
EVENTS
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'with a multi-valued query parameter' do
|
109
|
+
record do
|
110
|
+
get_hello(params: 'ary[]=1&ary[]=2')
|
111
|
+
end
|
112
|
+
|
113
|
+
event = collect_events(@tracer).first.to_yaml
|
114
|
+
expect(Diffy::Diff.new(<<~EVENT, event).to_s).to eq('')
|
115
|
+
---
|
116
|
+
:id: 1
|
117
|
+
:event: :call
|
118
|
+
:http_client_request:
|
119
|
+
:request_method: GET
|
120
|
+
:url: http://localhost:19292/hello
|
121
|
+
:headers:
|
122
|
+
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
123
|
+
Accept: "*/*"
|
124
|
+
User-Agent: Ruby
|
125
|
+
Connection: close
|
126
|
+
:message:
|
127
|
+
- :name: ary
|
128
|
+
:class: Array
|
129
|
+
:value: '["1", "2"]'
|
130
|
+
EVENT
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'with a URL encoded query parameter' do
|
134
|
+
msg = 'foo/bar?baz'
|
135
|
+
record do
|
136
|
+
get_hello(params: "msg=#{CGI.escape msg}")
|
137
|
+
end
|
138
|
+
|
139
|
+
event = collect_events(@tracer).first.to_yaml
|
140
|
+
expect(Diffy::Diff.new(<<~EVENT, event).to_s).to eq('')
|
141
|
+
---
|
142
|
+
:id: 1
|
143
|
+
:event: :call
|
144
|
+
:http_client_request:
|
145
|
+
:request_method: GET
|
146
|
+
:url: http://localhost:19292/hello
|
147
|
+
:headers:
|
148
|
+
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
149
|
+
Accept: "*/*"
|
150
|
+
User-Agent: Ruby
|
151
|
+
Connection: close
|
152
|
+
:message:
|
153
|
+
- :name: msg
|
154
|
+
:class: String
|
155
|
+
:value: #{msg}
|
156
|
+
EVENT
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -61,7 +61,7 @@ describe 'SQL events' do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
context 'while listing records' do
|
64
|
-
let(:test_line_number) {
|
64
|
+
let(:test_line_number) { 29 }
|
65
65
|
let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_GET_api_users_lists_the_users.appmap.json') }
|
66
66
|
|
67
67
|
context 'using Sequel ORM' do
|
data/spec/spec_helper.rb
CHANGED
@@ -14,3 +14,19 @@ require 'appmap'
|
|
14
14
|
RSpec.configure do |config|
|
15
15
|
config.example_status_persistence_file_path = "tmp/rspec_failed_examples.txt"
|
16
16
|
end
|
17
|
+
|
18
|
+
# Re-run the Rails specs without re-generating the data. This is useful for efficiently enhancing and
|
19
|
+
# debugging the test itself.
|
20
|
+
def use_existing_data?
|
21
|
+
ENV['USE_EXISTING_DATA'] == 'true'
|
22
|
+
end
|
23
|
+
|
24
|
+
shared_context 'collect events' do
|
25
|
+
def collect_events(tracer)
|
26
|
+
[].tap do |events|
|
27
|
+
while tracer.event?
|
28
|
+
events << tracer.next_event.to_h
|
29
|
+
end
|
30
|
+
end.map(&AppMap::Util.method(:sanitize_event))
|
31
|
+
end
|
32
|
+
end
|
@@ -11,8 +11,7 @@
|
|
11
11
|
"name": "sign",
|
12
12
|
"type": "function",
|
13
13
|
"location": "lib/openssl_key_sign.rb:10",
|
14
|
-
"static": true
|
15
|
-
"source": " def Example.sign\n key = OpenSSL::PKey::RSA.new 2048\n\n document = 'the document'\n\n digest = OpenSSL::Digest::SHA256.new\n key.sign digest, document\n end\n"
|
14
|
+
"static": true
|
16
15
|
}
|
17
16
|
]
|
18
17
|
}
|
@@ -40,8 +39,7 @@
|
|
40
39
|
"location": "OpenSSL::PKey::PKey#sign",
|
41
40
|
"static": false,
|
42
41
|
"labels": [
|
43
|
-
"
|
44
|
-
"crypto"
|
42
|
+
"crypto.pkey"
|
45
43
|
]
|
46
44
|
}
|
47
45
|
]
|
@@ -2,7 +2,7 @@ require 'rspec'
|
|
2
2
|
require 'appmap/rspec'
|
3
3
|
require 'hello'
|
4
4
|
|
5
|
-
describe Hello
|
5
|
+
describe Hello do
|
6
6
|
before do
|
7
7
|
# Trick appmap-ruby into thinking we're a Rails app.
|
8
8
|
stub_const('Rails', double('rails', version: 'fake.0'))
|
@@ -11,11 +11,11 @@ describe Hello, feature_group: 'Saying hello' do
|
|
11
11
|
# The order of these examples is important. The tests check the
|
12
12
|
# appmap for 'says hello', and we want another example to get run
|
13
13
|
# before it.
|
14
|
-
it 'does not say goodbye'
|
14
|
+
it 'does not say goodbye' do
|
15
15
|
expect(Hello.new.say_hello).not_to eq('Goodbye!')
|
16
16
|
end
|
17
17
|
|
18
|
-
it 'says hello'
|
18
|
+
it 'says hello' do
|
19
19
|
expect(Hello.new.say_hello).to eq('Hello!')
|
20
20
|
end
|
21
21
|
end
|