appsignal 3.0.3-java → 3.0.7-java
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/.semaphore/semaphore.yml +64 -199
- data/CHANGELOG.md +47 -19
- data/README.md +9 -3
- data/appsignal.gemspec +16 -3
- data/build_matrix.yml +16 -24
- data/ext/agent.yml +17 -17
- data/ext/base.rb +12 -1
- data/gemfiles/capistrano2.gemfile +0 -1
- data/gemfiles/capistrano3.gemfile +0 -1
- data/gemfiles/grape.gemfile +0 -1
- data/gemfiles/no_dependencies.gemfile +4 -1
- data/gemfiles/rails-3.2.gemfile +2 -0
- data/gemfiles/rails-4.2.gemfile +6 -0
- data/gemfiles/resque-2.gemfile +0 -4
- data/gemfiles/sequel-435.gemfile +0 -1
- data/gemfiles/sequel.gemfile +0 -1
- data/gemfiles/sinatra.gemfile +0 -1
- data/lib/appsignal/config.rb +1 -0
- data/lib/appsignal/hooks.rb +2 -1
- data/lib/appsignal/hooks/excon.rb +19 -0
- data/lib/appsignal/hooks/puma.rb +1 -16
- data/lib/appsignal/integrations/excon.rb +20 -0
- data/lib/appsignal/integrations/padrino.rb +1 -1
- data/lib/appsignal/integrations/railtie.rb +1 -1
- data/lib/appsignal/integrations/redis.rb +8 -5
- data/lib/appsignal/integrations/sinatra.rb +1 -1
- data/lib/appsignal/probes.rb +0 -1
- data/lib/appsignal/version.rb +1 -1
- data/lib/puma/plugin/appsignal.rb +146 -17
- data/mono.yml +16 -0
- data/spec/lib/appsignal/cli/diagnose_spec.rb +1 -0
- data/spec/lib/appsignal/hooks/excon_spec.rb +74 -0
- data/spec/lib/appsignal/hooks/puma_spec.rb +0 -46
- data/spec/lib/appsignal/hooks/redis_spec.rb +34 -10
- data/spec/lib/appsignal/hooks_spec.rb +4 -1
- data/spec/lib/puma/appsignal_spec.rb +244 -68
- data/support/install_deps +9 -8
- metadata +8 -6
- data/lib/appsignal/probes/puma.rb +0 -61
- data/spec/lib/appsignal/probes/puma_spec.rb +0 -180
@@ -35,29 +35,6 @@ describe Appsignal::Hooks::PumaHook do
|
|
35
35
|
# Does not error on call
|
36
36
|
Appsignal::Hooks::PumaHook.new.install
|
37
37
|
end
|
38
|
-
|
39
|
-
context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
|
40
|
-
before do
|
41
|
-
# Set in lib/puma/appsignal.rb
|
42
|
-
APPSIGNAL_PUMA_PLUGIN_LOADED = true
|
43
|
-
end
|
44
|
-
after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
|
45
|
-
|
46
|
-
it "does not add the Puma minutely probe" do
|
47
|
-
Appsignal::Hooks::PumaHook.new.install
|
48
|
-
expect(Appsignal::Minutely.probes[:puma]).to be_nil
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
|
53
|
-
it "adds the Puma minutely probe" do
|
54
|
-
expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
|
55
|
-
|
56
|
-
Appsignal::Hooks::PumaHook.new.install
|
57
|
-
probe = Appsignal::Minutely.probes[:puma]
|
58
|
-
expect(probe).to eql(Appsignal::Probes::PumaProbe)
|
59
|
-
end
|
60
|
-
end
|
61
38
|
end
|
62
39
|
|
63
40
|
context "when in clustered mode" do
|
@@ -81,29 +58,6 @@ describe Appsignal::Hooks::PumaHook do
|
|
81
58
|
cluster.stop_workers
|
82
59
|
expect(cluster.instance_variable_get(:@called)).to be(true)
|
83
60
|
end
|
84
|
-
|
85
|
-
context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
|
86
|
-
before do
|
87
|
-
# Set in lib/puma/appsignal.rb
|
88
|
-
APPSIGNAL_PUMA_PLUGIN_LOADED = true
|
89
|
-
end
|
90
|
-
after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
|
91
|
-
|
92
|
-
it "does not add the Puma minutely probe" do
|
93
|
-
Appsignal::Hooks::PumaHook.new.install
|
94
|
-
expect(Appsignal::Minutely.probes[:puma]).to be_nil
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
|
99
|
-
it "adds the Puma minutely probe" do
|
100
|
-
expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
|
101
|
-
|
102
|
-
Appsignal::Hooks::PumaHook.new.install
|
103
|
-
probe = Appsignal::Minutely.probes[:puma]
|
104
|
-
expect(probe).to eql(Appsignal::Probes::PumaProbe)
|
105
|
-
end
|
106
|
-
end
|
107
61
|
end
|
108
62
|
end
|
109
63
|
end
|
@@ -33,16 +33,17 @@ describe Appsignal::Hooks::RedisHook do
|
|
33
33
|
|
34
34
|
context "instrumentation" do
|
35
35
|
before do
|
36
|
+
start_agent
|
36
37
|
# Stub Redis::Client class so that it doesn't perform an actual
|
37
38
|
# Redis query. This class will be included (prepended) with the
|
38
39
|
# AppSignal Redis integration.
|
39
40
|
stub_const("Redis::Client", Class.new do
|
40
41
|
def id
|
41
|
-
|
42
|
+
"stub_id"
|
42
43
|
end
|
43
44
|
|
44
|
-
def
|
45
|
-
|
45
|
+
def write(_commands)
|
46
|
+
"stub_write"
|
46
47
|
end
|
47
48
|
end)
|
48
49
|
# Load the integration again for the stubbed Redis::Client class.
|
@@ -50,17 +51,40 @@ describe Appsignal::Hooks::RedisHook do
|
|
50
51
|
# track if it was installed already or not.
|
51
52
|
Appsignal::Hooks::RedisHook.new.install
|
52
53
|
end
|
54
|
+
let!(:transaction) do
|
55
|
+
Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
|
56
|
+
end
|
57
|
+
around { |example| keep_transactions { example.run } }
|
53
58
|
|
54
59
|
it "instrument a redis call" do
|
55
|
-
|
56
|
-
expect(
|
57
|
-
.at_least(:once)
|
58
|
-
expect(Appsignal::Transaction.current).to receive(:finish_event)
|
59
|
-
.at_least(:once)
|
60
|
-
.with("query.redis", :stub_id, "get ?", 0)
|
60
|
+
client = Redis::Client.new
|
61
|
+
expect(client.write([:get, "key"])).to eql("stub_write")
|
61
62
|
|
63
|
+
transaction_hash = transaction.to_h
|
64
|
+
expect(transaction_hash["events"]).to include(
|
65
|
+
hash_including(
|
66
|
+
"name" => "query.redis",
|
67
|
+
"body" => "get ?",
|
68
|
+
"title" => "stub_id"
|
69
|
+
)
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "instrument a redis script call" do
|
62
74
|
client = Redis::Client.new
|
63
|
-
|
75
|
+
script = "return redis.call('set',KEYS[1],ARGV[1])"
|
76
|
+
keys = ["foo"]
|
77
|
+
argv = ["bar"]
|
78
|
+
expect(client.write([:eval, script, keys.size, keys, argv])).to eql("stub_write")
|
79
|
+
|
80
|
+
transaction_hash = transaction.to_h
|
81
|
+
expect(transaction_hash["events"]).to include(
|
82
|
+
hash_including(
|
83
|
+
"name" => "query.redis",
|
84
|
+
"body" => "#{script} ? ?",
|
85
|
+
"title" => "stub_id"
|
86
|
+
)
|
87
|
+
)
|
64
88
|
end
|
65
89
|
end
|
66
90
|
end
|
@@ -68,7 +68,10 @@ describe Appsignal::Hooks do
|
|
68
68
|
expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
|
69
69
|
|
70
70
|
expect(Appsignal.logger).to receive(:error).with("Error while installing mock_error_hook hook: error").once
|
71
|
-
expect(Appsignal.logger).to receive(:debug).
|
71
|
+
expect(Appsignal.logger).to receive(:debug).ordered do |message|
|
72
|
+
expect(message).to eq("Installing mock_error_hook hook")
|
73
|
+
end
|
74
|
+
expect(Appsignal.logger).to receive(:debug).ordered do |message|
|
72
75
|
# Start of the error backtrace as debug log
|
73
76
|
expect(message).to start_with(File.expand_path("../../../../", __FILE__))
|
74
77
|
end
|
@@ -1,6 +1,4 @@
|
|
1
1
|
RSpec.describe "Puma plugin" do
|
2
|
-
include WaitForHelper
|
3
|
-
|
4
2
|
class MockPumaLauncher
|
5
3
|
def events
|
6
4
|
return @events if defined?(@events)
|
@@ -10,110 +8,288 @@ RSpec.describe "Puma plugin" do
|
|
10
8
|
end
|
11
9
|
|
12
10
|
class MockPumaEvents
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
attr_reader :logs
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@logs = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def log(message)
|
18
|
+
@logs << [:log, message]
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug(message)
|
22
|
+
@logs << [:debug, message]
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(message)
|
26
|
+
@logs << [:error, message]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# StatsD server used for these tests.
|
31
|
+
# Open a UDPSocket and listen for messages sent by the AppSignal Puma plugin.
|
32
|
+
class StatsdServer
|
33
|
+
def start
|
34
|
+
stop
|
35
|
+
@socket = UDPSocket.new
|
36
|
+
@socket.bind("127.0.0.1", 8125)
|
37
|
+
|
38
|
+
loop do
|
39
|
+
begin
|
40
|
+
# Listen for messages and track them on the messages Array.
|
41
|
+
packet = @socket.recvfrom(1024)
|
42
|
+
track_message packet.first
|
43
|
+
rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
|
44
|
+
# Ignore error for JRuby 9.1.17.0 specifically, it doesn't appear to
|
45
|
+
# happen on 9.2.18.0. It doesn't break the tests themselves, ignoring
|
46
|
+
# this error. It's probably a timing issue where it tries to read
|
47
|
+
# from the socket after it's closed.
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop
|
53
|
+
@socket && @socket.close
|
54
|
+
ensure
|
55
|
+
@socket = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def messages
|
59
|
+
@messages ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def track_message(message)
|
65
|
+
@messages_mutex ||= Mutex.new
|
66
|
+
@messages_mutex.synchronize { messages << message }
|
16
67
|
end
|
17
68
|
end
|
18
69
|
|
19
70
|
let(:probe) { MockProbe.new }
|
20
71
|
let(:launcher) { MockPumaLauncher.new }
|
72
|
+
let(:hostname) { Socket.gethostname }
|
73
|
+
let(:expected_default_tags) { { "hostname" => hostname } }
|
74
|
+
let(:stats_data) { { :backlog => 1 } }
|
21
75
|
before do
|
22
76
|
module Puma
|
23
77
|
def self.stats
|
78
|
+
JSON.dump(@_stats_data)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.stats_hash
|
82
|
+
@_stats_data
|
24
83
|
end
|
25
84
|
|
26
|
-
def self.
|
27
|
-
|
28
|
-
before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
|
29
|
-
|
30
|
-
# An abbreviated version of what happens in Puma::Cluster#run
|
31
|
-
launcher = MockPumaLauncher.new
|
32
|
-
plugin = Plugin.plugin.new
|
33
|
-
plugin.start(launcher)
|
34
|
-
launcher.events.on_booted.call
|
35
|
-
|
36
|
-
# Wait for minutely probe thread to finish starting
|
37
|
-
sleep 0.005
|
38
|
-
|
39
|
-
# Capture any new threads running after application is preloaded.
|
40
|
-
# Any threads created during the preloading phase will not be
|
41
|
-
# carried over into the forked workers. Puma warns about these
|
42
|
-
# but the minutely probe thread should only exist in the main process.
|
43
|
-
after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
|
44
|
-
$stdout.puts "! WARNING: Detected #{after.size - before.size} Thread(s) started in app boot" if after.size > before.size
|
85
|
+
def self._set_stats=(data)
|
86
|
+
@_stats_data = data
|
45
87
|
end
|
46
88
|
|
47
89
|
class Plugin
|
48
90
|
class << self
|
49
|
-
attr_reader :
|
91
|
+
attr_reader :appsignal_plugin
|
50
92
|
|
51
93
|
def create(&block)
|
52
|
-
@
|
53
|
-
@
|
94
|
+
@appsignal_plugin = Class.new(::Puma::Plugin)
|
95
|
+
@appsignal_plugin.class_eval(&block)
|
54
96
|
end
|
55
97
|
end
|
56
|
-
end
|
57
|
-
end
|
58
98
|
|
59
|
-
|
60
|
-
ENV["APPSIGNAL_ENABLE_MINUTELY_PROBES"] = "true"
|
61
|
-
Appsignal.config = project_fixture_config
|
62
|
-
# Speed up test time
|
63
|
-
allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
|
64
|
-
allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
|
99
|
+
attr_reader :in_background_block
|
65
100
|
|
66
|
-
|
101
|
+
def in_background(&block)
|
102
|
+
@in_background_block = block
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
Puma._set_stats = stats_data
|
67
107
|
load File.expand_path("../lib/puma/plugin/appsignal.rb", APPSIGNAL_SPEC_DIR)
|
108
|
+
|
109
|
+
@statsd = StatsdServer.new
|
110
|
+
@server_thread = Thread.new { @statsd.start }
|
111
|
+
@server_thread.abort_on_exception = true
|
68
112
|
end
|
69
113
|
after do
|
70
|
-
|
71
|
-
|
72
|
-
Object.send
|
114
|
+
@statsd = nil
|
115
|
+
|
116
|
+
Object.send(:remove_const, :Puma)
|
117
|
+
Object.send(:remove_const, :AppsignalPumaPlugin)
|
118
|
+
end
|
119
|
+
|
120
|
+
def run(plugin)
|
121
|
+
@client_thread = Thread.new { start_plugin(plugin) }
|
122
|
+
@client_thread.abort_on_exception = true
|
123
|
+
sleep 0.03
|
124
|
+
ensure
|
125
|
+
stop_all
|
73
126
|
end
|
74
127
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
plugin = Puma::Plugin.plugin.new
|
79
|
-
expect(launcher.events.on_booted).to be_nil
|
128
|
+
def appsignal_plugin
|
129
|
+
Puma::Plugin.appsignal_plugin
|
130
|
+
end
|
80
131
|
|
132
|
+
def start_plugin(plugin_class)
|
133
|
+
plugin = plugin_class.new
|
134
|
+
# Speed up test by not waiting for 60 seconds initial wait time and loop
|
135
|
+
# interval.
|
136
|
+
allow(plugin).to receive(:sleep_time).and_return(0.01)
|
81
137
|
plugin.start(launcher)
|
82
|
-
|
83
|
-
|
138
|
+
plugin.in_background_block.call
|
139
|
+
end
|
140
|
+
|
141
|
+
# Stop all threads in test and stop listening on the UDPSocket
|
142
|
+
def stop_all
|
143
|
+
@client_thread.kill if defined?(@client_thread) && @client_thread
|
144
|
+
@server_thread.kill if defined?(@server_thread) && @server_thread
|
145
|
+
@statsd.stop if defined?(@statsd) && @statsd
|
146
|
+
@client_thread = nil
|
147
|
+
@server_thread = nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def logs
|
151
|
+
launcher.events.logs
|
152
|
+
end
|
153
|
+
|
154
|
+
def messages
|
155
|
+
@statsd.messages.map do |message|
|
156
|
+
metric, type, tags_string = message.split("|")
|
157
|
+
metric_name, metric_value = metric.split(":")
|
158
|
+
tags = {}
|
159
|
+
tags_string[1..-1].split(",").each do |tag|
|
160
|
+
key, value = tag.split(":")
|
161
|
+
tags[key] = value
|
162
|
+
end
|
163
|
+
{
|
164
|
+
:name => metric_name,
|
165
|
+
:value => metric_value.to_i,
|
166
|
+
:type => type,
|
167
|
+
:tags => tags
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def expect_gauge(metric_name, metric_value, tags_hash = {})
|
173
|
+
expect(messages).to include(
|
174
|
+
:name => "puma_#{metric_name}",
|
175
|
+
:value => metric_value,
|
176
|
+
:type => "g",
|
177
|
+
:tags => expected_default_tags.merge(tags_hash)
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
context "with multiple worker stats" do
|
182
|
+
let(:stats_data) do
|
183
|
+
{
|
184
|
+
:workers => 2,
|
185
|
+
:booted_workers => 2,
|
186
|
+
:old_workers => 0,
|
187
|
+
:worker_status => [
|
188
|
+
{
|
189
|
+
:last_status => {
|
190
|
+
:backlog => 0,
|
191
|
+
:running => 5,
|
192
|
+
:pool_capacity => 5,
|
193
|
+
:max_threads => 5
|
194
|
+
}
|
195
|
+
},
|
196
|
+
{
|
197
|
+
:last_status => {
|
198
|
+
:backlog => 0,
|
199
|
+
:running => 5,
|
200
|
+
:pool_capacity => 5,
|
201
|
+
:max_threads => 5
|
202
|
+
}
|
203
|
+
}
|
204
|
+
]
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
it "collects puma stats as guage metrics with the (summed) worker metrics" do
|
209
|
+
run(appsignal_plugin)
|
210
|
+
|
211
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
212
|
+
expect_gauge(:workers, 2, "type" => "count")
|
213
|
+
expect_gauge(:workers, 2, "type" => "booted")
|
214
|
+
expect_gauge(:workers, 0, "type" => "old")
|
215
|
+
expect_gauge(:connection_backlog, 0)
|
216
|
+
expect_gauge(:pool_capacity, 10)
|
217
|
+
expect_gauge(:threads, 10, "type" => "running")
|
218
|
+
expect_gauge(:threads, 10, "type" => "max")
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context "with single worker stats" do
|
223
|
+
let(:stats_data) do
|
224
|
+
{
|
225
|
+
:backlog => 0,
|
226
|
+
:running => 5,
|
227
|
+
:pool_capacity => 5,
|
228
|
+
:max_threads => 5
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
it "calls `puma_gauge` with the (summed) worker metrics" do
|
233
|
+
run(appsignal_plugin)
|
234
|
+
|
235
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
236
|
+
expect_gauge(:connection_backlog, 0)
|
237
|
+
expect_gauge(:pool_capacity, 5)
|
238
|
+
expect_gauge(:threads, 5, "type" => "running")
|
239
|
+
expect_gauge(:threads, 5, "type" => "max")
|
240
|
+
end
|
241
|
+
end
|
84
242
|
|
85
|
-
|
86
|
-
|
243
|
+
context "when using APPSIGNAL_HOSTNAME" do
|
244
|
+
let(:hostname) { "my-host-name" }
|
245
|
+
before { ENV["APPSIGNAL_HOSTNAME"] = hostname }
|
246
|
+
after { ENV.delete("APPSIGNAL_HOSTNAME") }
|
87
247
|
|
88
|
-
|
89
|
-
|
248
|
+
it "reports the APPSIGNAL_HOSTNAME as the hostname tag value" do
|
249
|
+
run(appsignal_plugin)
|
250
|
+
|
251
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
252
|
+
expect_gauge(:connection_backlog, 1)
|
253
|
+
end
|
90
254
|
end
|
91
255
|
|
92
|
-
|
93
|
-
|
94
|
-
|
256
|
+
context "without Puma.stats_hash" do
|
257
|
+
before do
|
258
|
+
Puma.singleton_class.send(:remove_method, :stats_hash)
|
259
|
+
end
|
260
|
+
|
261
|
+
it "fetches metrics from Puma.stats instead" do
|
262
|
+
run(appsignal_plugin)
|
95
263
|
|
96
|
-
|
264
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
265
|
+
expect(logs).to_not include([kind_of(Symbol), "AppSignal: No Puma stats to report."])
|
266
|
+
expect_gauge(:connection_backlog, 1)
|
267
|
+
end
|
97
268
|
end
|
98
269
|
|
99
|
-
context "without Puma.stats" do
|
100
|
-
before
|
270
|
+
context "without Puma.stats and Puma.stats_hash" do
|
271
|
+
before do
|
272
|
+
Puma.singleton_class.send(:remove_method, :stats)
|
273
|
+
Puma.singleton_class.send(:remove_method, :stats_hash)
|
274
|
+
end
|
101
275
|
|
102
|
-
it "does not
|
103
|
-
|
104
|
-
expect(Appsignal::Minutely.probes[:puma]).to be_nil
|
105
|
-
plugin = Puma::Plugin.plugin.new
|
106
|
-
expect(launcher.events.on_booted).to be_nil
|
276
|
+
it "does not fetch metrics" do
|
277
|
+
run(appsignal_plugin)
|
107
278
|
|
108
|
-
|
109
|
-
expect(
|
110
|
-
expect(
|
279
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
280
|
+
expect(logs).to include([:log, "AppSignal: No Puma stats to report."])
|
281
|
+
expect(messages).to be_empty
|
282
|
+
end
|
283
|
+
end
|
111
284
|
|
112
|
-
|
113
|
-
|
285
|
+
context "without running StatsD server" do
|
286
|
+
it "does nothing" do
|
287
|
+
Appsignal.stop
|
288
|
+
stop_all
|
289
|
+
run(appsignal_plugin)
|
114
290
|
|
115
|
-
|
116
|
-
|
291
|
+
expect(logs).to_not include([:error, kind_of(String)])
|
292
|
+
expect(messages).to be_empty
|
117
293
|
end
|
118
294
|
end
|
119
295
|
end
|