logstash-core 2.4.1-java → 5.0.0.alpha1-java

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of logstash-core might be problematic. Click here for more details.

Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/lib/logstash-core/version.rb +1 -1
  3. data/lib/logstash/agent.rb +124 -411
  4. data/lib/logstash/api/init.ru +31 -0
  5. data/lib/logstash/api/lib/app.rb +40 -0
  6. data/lib/logstash/api/lib/app/command.rb +29 -0
  7. data/lib/logstash/api/lib/app/command_factory.rb +29 -0
  8. data/lib/logstash/api/lib/app/commands/stats/events_command.rb +13 -0
  9. data/lib/logstash/api/lib/app/commands/stats/hotthreads_command.rb +120 -0
  10. data/lib/logstash/api/lib/app/commands/stats/memory_command.rb +25 -0
  11. data/lib/logstash/api/lib/app/commands/system/basicinfo_command.rb +15 -0
  12. data/lib/logstash/api/lib/app/commands/system/plugins_command.rb +28 -0
  13. data/lib/logstash/api/lib/app/modules/node.rb +25 -0
  14. data/lib/logstash/api/lib/app/modules/node_stats.rb +51 -0
  15. data/lib/logstash/api/lib/app/modules/plugins.rb +15 -0
  16. data/lib/logstash/api/lib/app/modules/stats.rb +21 -0
  17. data/lib/logstash/api/lib/app/root.rb +13 -0
  18. data/lib/logstash/api/lib/app/service.rb +61 -0
  19. data/lib/logstash/api/lib/app/stats.rb +56 -0
  20. data/lib/logstash/api/lib/helpers/app_helpers.rb +23 -0
  21. data/lib/logstash/codecs/base.rb +1 -29
  22. data/lib/logstash/config/config_ast.rb +18 -31
  23. data/lib/logstash/config/loader.rb +3 -5
  24. data/lib/logstash/config/mixin.rb +25 -64
  25. data/lib/logstash/filter_delegator.rb +65 -0
  26. data/lib/logstash/inputs/base.rb +1 -1
  27. data/lib/logstash/inputs/metrics.rb +47 -0
  28. data/lib/logstash/instrument/collector.rb +109 -0
  29. data/lib/logstash/instrument/metric.rb +102 -0
  30. data/lib/logstash/instrument/metric_store.rb +228 -0
  31. data/lib/logstash/instrument/metric_type.rb +24 -0
  32. data/lib/logstash/instrument/metric_type/base.rb +35 -0
  33. data/lib/logstash/instrument/metric_type/counter.rb +29 -0
  34. data/lib/logstash/instrument/metric_type/gauge.rb +22 -0
  35. data/lib/logstash/instrument/metric_type/mean.rb +33 -0
  36. data/lib/logstash/instrument/namespaced_metric.rb +54 -0
  37. data/lib/logstash/instrument/null_metric.rb +4 -3
  38. data/lib/logstash/instrument/periodic_poller/base.rb +57 -0
  39. data/lib/logstash/instrument/periodic_poller/jvm.rb +92 -0
  40. data/lib/logstash/instrument/periodic_poller/os.rb +13 -0
  41. data/lib/logstash/instrument/periodic_poller/periodic_poller_observer.rb +19 -0
  42. data/lib/logstash/instrument/periodic_pollers.rb +26 -0
  43. data/lib/logstash/instrument/snapshot.rb +16 -0
  44. data/lib/logstash/json.rb +2 -3
  45. data/lib/logstash/namespace.rb +1 -0
  46. data/lib/logstash/output_delegator.rb +16 -3
  47. data/lib/logstash/outputs/base.rb +1 -32
  48. data/lib/logstash/pipeline.rb +67 -8
  49. data/lib/logstash/plugin.rb +57 -19
  50. data/lib/logstash/runner.rb +348 -84
  51. data/lib/logstash/util.rb +9 -0
  52. data/lib/logstash/util/duration_formatter.rb +15 -0
  53. data/lib/logstash/util/java_version.rb +2 -4
  54. data/lib/logstash/util/loggable.rb +29 -0
  55. data/lib/logstash/version.rb +1 -1
  56. data/lib/logstash/webserver.rb +98 -0
  57. data/locales/en.yml +42 -24
  58. data/logstash-core.gemspec +9 -6
  59. data/spec/api/lib/api/node_spec.rb +64 -0
  60. data/spec/api/lib/api/node_stats_spec.rb +68 -0
  61. data/spec/api/lib/api/plugins_spec.rb +57 -0
  62. data/spec/api/lib/api/root_spec.rb +20 -0
  63. data/spec/api/lib/api/stats_spec.rb +19 -0
  64. data/spec/api/lib/commands/events_spec.rb +17 -0
  65. data/spec/api/lib/commands/jvm_spec.rb +45 -0
  66. data/spec/api/spec_helper.rb +128 -0
  67. data/spec/logstash/agent_spec.rb +62 -169
  68. data/spec/logstash/config/config_ast_spec.rb +2 -47
  69. data/spec/logstash/config/mixin_spec.rb +0 -157
  70. data/spec/logstash/filter_delegator_spec.rb +143 -0
  71. data/spec/logstash/inputs/metrics_spec.rb +52 -0
  72. data/spec/logstash/instrument/collector_spec.rb +49 -0
  73. data/spec/logstash/instrument/metric_spec.rb +110 -0
  74. data/spec/logstash/instrument/metric_store_spec.rb +163 -0
  75. data/spec/logstash/instrument/metric_type/counter_spec.rb +40 -0
  76. data/spec/logstash/instrument/metric_type/gauge_spec.rb +40 -0
  77. data/spec/logstash/instrument/namespaced_metric_spec.rb +25 -0
  78. data/spec/logstash/instrument/null_metric_spec.rb +9 -51
  79. data/spec/logstash/json_spec.rb +14 -0
  80. data/spec/logstash/output_delegator_spec.rb +6 -3
  81. data/spec/logstash/outputs/base_spec.rb +0 -107
  82. data/spec/logstash/pipeline_spec.rb +204 -33
  83. data/spec/logstash/plugin_spec.rb +80 -15
  84. data/spec/logstash/runner_spec.rb +134 -38
  85. data/spec/logstash/shutdown_watcher_spec.rb +0 -1
  86. data/spec/logstash/util/duration_formatter_spec.rb +11 -0
  87. data/spec/logstash/util/java_version_spec.rb +10 -2
  88. data/spec/logstash/util_spec.rb +28 -0
  89. data/spec/support/matchers.rb +30 -0
  90. metadata +154 -20
  91. data/lib/logstash/logging/json.rb +0 -21
  92. data/lib/logstash/special_agent.rb +0 -8
  93. data/lib/logstash/util/safe_uri.rb +0 -50
  94. data/spec/logstash/codecs/base_spec.rb +0 -74
  95. data/spec/static/i18n_spec.rb +0 -25
@@ -1,18 +1,19 @@
1
1
  # encoding: utf-8
2
+ require "spec_helper"
2
3
  require "logstash/plugin"
4
+ require "logstash/outputs/base"
5
+ require "logstash/codecs/base"
3
6
  require "logstash/inputs/base"
4
7
  require "logstash/filters/base"
5
- require "logstash/outputs/base"
6
- require "spec_helper"
7
8
 
8
9
  describe LogStash::Plugin do
9
10
  it "should fail lookup on inexisting type" do
10
- #expect_any_instance_of(Cabin::Channel).to receive(:debug).once
11
+ expect_any_instance_of(Cabin::Channel).to receive(:debug).once
11
12
  expect { LogStash::Plugin.lookup("badbadtype", "badname") }.to raise_error(LogStash::PluginLoadingError)
12
13
  end
13
14
 
14
15
  it "should fail lookup on inexisting name" do
15
- #expect_any_instance_of(Cabin::Channel).to receive(:debug).once
16
+ expect_any_instance_of(Cabin::Channel).to receive(:debug).once
16
17
  expect { LogStash::Plugin.lookup("filter", "badname") }.to raise_error(LogStash::PluginLoadingError)
17
18
  end
18
19
 
@@ -169,26 +170,90 @@ describe LogStash::Plugin do
169
170
 
170
171
  end
171
172
  end
172
- context "Collecting Metric in the plugin" do
173
- [LogStash::Inputs::Base, LogStash::Filters::Base, LogStash::Outputs::Base].each do |type|
173
+
174
+ describe "#id" do
175
+ plugin_types = [
176
+ LogStash::Filters::Base,
177
+ LogStash::Codecs::Base,
178
+ LogStash::Outputs::Base,
179
+ LogStash::Inputs::Base
180
+ ]
181
+
182
+ plugin_types.each do |plugin_type|
174
183
  let(:plugin) do
175
- Class.new(type) do
176
- config_name "goku"
184
+ Class.new(plugin_type) do
185
+ config_name "simple_plugin"
186
+
187
+ config :host, :validate => :string
188
+ config :export, :validte => :boolean
189
+
190
+ def register; end
191
+ end
192
+ end
193
+
194
+ let(:config) do
195
+ {
196
+ "host" => "127.0.0.1",
197
+ "export" => true
198
+ }
199
+ end
200
+
201
+ subject { plugin.new(config) }
202
+
203
+ context "plugin type is #{plugin_type}" do
204
+ context "when there is not ID configured for the output" do
205
+ it "it uses a UUID to identify this plugins" do
206
+ expect(subject.id).not_to eq(nil)
207
+ end
177
208
 
178
- def register
179
- metric.gauge("power-level", 9000)
209
+ it "will be different between instance of plugins" do
210
+ expect(subject.id).not_to eq(plugin.new(config).id)
211
+ end
212
+ end
213
+
214
+ context "When a user provide an ID for the plugin" do
215
+ let(:id) { "ABC" }
216
+ let(:config) { super.merge("id" => id) }
217
+
218
+ it "uses the user provided ID" do
219
+ expect(subject.id).to eq(id)
180
220
  end
181
221
  end
182
222
  end
223
+ end
224
+ end
183
225
 
184
- subject { plugin.new }
226
+ describe "#plugin_unique_name" do
227
+ let(:plugin) do
228
+ Class.new(LogStash::Filters::Base,) do
229
+ config_name "simple_plugin"
230
+ config :host, :validate => :string
185
231
 
186
- it "should not raise an exception when recoding a metric" do
187
- expect { subject.register }.not_to raise_error
232
+ def register; end
188
233
  end
234
+ end
235
+
236
+ let(:config) do
237
+ {
238
+ "host" => "127.0.0.1"
239
+ }
240
+ end
241
+
242
+ context "when the id is provided" do
243
+ let(:my_id) { "mysuper-plugin" }
244
+ let(:config) { super.merge({ "id" => my_id })}
245
+ subject { plugin.new(config) }
246
+
247
+ it "return a human readable ID" do
248
+ expect(subject.plugin_unique_name).to eq("simple_plugin_#{my_id}")
249
+ end
250
+ end
251
+
252
+ context "when the id is not provided provided" do
253
+ subject { plugin.new(config) }
189
254
 
190
- it "should use a `NullMetric`" do
191
- expect(subject.metric).to be_kind_of(LogStash::Instrument::NullMetric)
255
+ it "return a human readable ID" do
256
+ expect(subject.plugin_unique_name).to match(/^simple_plugin_/)
192
257
  end
193
258
  end
194
259
  end
@@ -3,7 +3,7 @@ require "spec_helper"
3
3
  require "logstash/runner"
4
4
  require "stud/task"
5
5
  require "stud/trap"
6
- require "stud/temporary"
6
+ require "logstash/util/java_version"
7
7
 
8
8
  class NullRunner
9
9
  def run(args); end
@@ -11,73 +11,169 @@ end
11
11
 
12
12
  describe LogStash::Runner do
13
13
 
14
+ subject { LogStash::Runner }
14
15
  let(:channel) { Cabin::Channel.new }
15
16
 
16
17
  before :each do
17
18
  allow(Cabin::Channel).to receive(:get).with(LogStash).and_return(channel)
18
- allow(channel).to receive(:subscribe).with(any_args).and_call_original
19
+ allow(channel).to receive(:subscribe).with(any_args)
19
20
  end
20
21
 
21
- context "argument parsing" do
22
- it "should run agent" do
23
- expect(Stud::Task).to receive(:new).once.and_return(nil)
24
- args = ["agent", "-e", ""]
25
- expect(subject.run(args)).to eq(nil)
22
+ describe "argument parsing" do
23
+ subject { LogStash::Runner.new("") }
24
+ context "when -e is given" do
25
+
26
+ let(:args) { ["-e", "input {} output {}"] }
27
+ let(:agent) { double("agent") }
28
+ let(:agent_logger) { double("agent logger") }
29
+
30
+ before do
31
+ allow(agent).to receive(:logger=).with(anything)
32
+ allow(agent).to receive(:shutdown)
33
+ allow(agent).to receive(:register_pipeline)
34
+ end
35
+
36
+ it "should execute the agent" do
37
+ expect(subject).to receive(:create_agent).and_return(agent)
38
+ expect(agent).to receive(:execute).once
39
+ subject.run(args)
40
+ end
41
+ end
42
+
43
+ context "with no arguments" do
44
+ let(:args) { [] }
45
+ let(:agent) { double("agent") }
46
+
47
+ before(:each) do
48
+ allow(LogStash::Agent).to receive(:new).and_return(agent)
49
+ allow(LogStash::Util::JavaVersion).to receive(:warn_on_bad_java_version)
50
+ end
51
+
52
+ it "should show help" do
53
+ expect($stderr).to receive(:puts).once
54
+ expect(subject).to receive(:signal_usage_error).once.and_call_original
55
+ expect(subject).to receive(:show_short_help).once
56
+ subject.run(args)
57
+ end
26
58
  end
59
+ end
27
60
 
28
- it "should run agent help" do
29
- expect(subject).to receive(:show_help).once.and_return(nil)
30
- args = ["agent", "-h"]
31
- expect(subject.run(args).wait).to eq(0)
61
+ context "--pluginpath" do
62
+ subject { LogStash::Runner.new("") }
63
+ let(:single_path) { "/some/path" }
64
+ let(:multiple_paths) { ["/some/path1", "/some/path2"] }
65
+
66
+ it "should add single valid dir path to the environment" do
67
+ expect(File).to receive(:directory?).and_return(true)
68
+ expect(LogStash::Environment).to receive(:add_plugin_path).with(single_path)
69
+ subject.configure_plugin_paths(single_path)
32
70
  end
33
71
 
34
- it "should show help with no arguments" do
35
- expect($stderr).to receive(:puts).once.and_return("No command given")
36
- expect($stderr).to receive(:puts).once
37
- args = []
38
- expect(subject.run(args).wait).to eq(1)
72
+ it "should fail with single invalid dir path" do
73
+ expect(File).to receive(:directory?).and_return(false)
74
+ expect(LogStash::Environment).not_to receive(:add_plugin_path)
75
+ expect{subject.configure_plugin_paths(single_path)}.to raise_error(Clamp::UsageError)
39
76
  end
40
77
 
41
- it "should show help for unknown commands" do
42
- expect($stderr).to receive(:puts).once.and_return("No such command welp")
43
- expect($stderr).to receive(:puts).once
44
- args = ["welp"]
45
- expect(subject.run(args).wait).to eq(1)
78
+ it "should add multiple valid dir path to the environment" do
79
+ expect(File).to receive(:directory?).exactly(multiple_paths.size).times.and_return(true)
80
+ multiple_paths.each{|path| expect(LogStash::Environment).to receive(:add_plugin_path).with(path)}
81
+ subject.configure_plugin_paths(multiple_paths)
46
82
  end
47
83
  end
48
84
 
49
85
  context "--auto-reload" do
86
+ subject { LogStash::Runner.new("") }
50
87
  context "when -f is not given" do
51
88
 
52
- let(:args) { ["agent", "-r", "-e", "input {} output {}"] }
89
+ let(:args) { ["-r", "-e", "input {} output {}"] }
53
90
 
54
91
  it "should exit immediately" do
55
- expect(subject.run(args).wait).to eq(1)
92
+ expect(subject).to receive(:signal_usage_error).and_call_original
93
+ expect(subject).to receive(:show_short_help)
94
+ expect(subject.run(args)).to eq(1)
56
95
  end
57
96
  end
58
97
  end
59
98
 
60
- context "--log-in-json" do
61
- let(:logfile) { Stud::Temporary.file }
62
- let(:args) { [ "agent", "--log-in-json", "-l", logfile.path, "-e", "some-invalid-config" ] }
99
+ describe "--config-test" do
100
+ subject { LogStash::Runner.new("") }
101
+ let(:args) { ["-t", "-e", pipeline_string] }
63
102
 
64
- after do
65
- logfile.close
66
- File.unlink(logfile.path)
103
+ context "with a good configuration" do
104
+ let(:pipeline_string) { "input { } filter { } output { }" }
105
+ it "should exit successfuly" do
106
+ expect(channel).to receive(:terminal)
107
+ expect(subject.run(args)).to eq(0)
108
+ end
67
109
  end
68
110
 
69
- before do
70
- expect(channel).to receive(:subscribe).with(kind_of(LogStash::Logging::JSON)).and_call_original
71
- subject.run(args).wait
111
+ context "with a bad configuration" do
112
+ let(:pipeline_string) { "rlwekjhrewlqrkjh" }
113
+ it "should fail by returning a bad exit code" do
114
+ expect(channel).to receive(:fatal)
115
+ expect(subject.run(args)).to eq(1)
116
+ end
117
+ end
118
+ end
119
+ describe "pipeline settings" do
120
+ let(:pipeline_string) { "input { stdin {} } output { stdout {} }" }
121
+ let(:main_pipeline_settings) { { :pipeline_id => "main" } }
122
+ let(:pipeline) { double("pipeline") }
123
+
124
+ before(:each) do
125
+ allow_any_instance_of(LogStash::Agent).to receive(:execute).and_return(true)
126
+ task = Stud::Task.new { 1 }
127
+ allow(pipeline).to receive(:run).and_return(task)
128
+ allow(pipeline).to receive(:shutdown)
129
+ end
130
+
131
+ context "when :pipeline_workers is not defined by the user" do
132
+ it "should not pass the value to the pipeline" do
133
+ expect(LogStash::Pipeline).to receive(:new).once.with(pipeline_string, hash_excluding(:pipeline_workers)).and_return(pipeline)
134
+
135
+ args = ["-e", pipeline_string]
136
+ subject.run("bin/logstash", args)
137
+ end
138
+ end
72
139
 
73
- # Log file should have stuff in it.
74
- expect(logfile.stat.size).to be > 0
140
+ context "when :pipeline_workers is defined by the user" do
141
+ it "should pass the value to the pipeline" do
142
+ main_pipeline_settings[:pipeline_workers] = 2
143
+ expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(main_pipeline_settings)).and_return(pipeline)
144
+
145
+ args = ["-w", "2", "-e", pipeline_string]
146
+ subject.run("bin/logstash", args)
147
+ end
75
148
  end
76
149
 
77
- it "should log in valid json. One object per line." do
78
- logfile.each_line do |line|
79
- expect(line).not_to be_empty
80
- expect { LogStash::Json.load(line) }.not_to raise_error
150
+ describe "debug_config" do
151
+ it "should set 'debug_config' to false by default" do
152
+ expect(LogStash::Config::Loader).to receive(:new).with(anything, false).and_call_original
153
+ expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:debug_config => false)).and_return(pipeline)
154
+ args = ["--debug", "-e", pipeline_string]
155
+ subject.run("bin/logstash", args)
156
+ end
157
+
158
+ it "should allow overriding debug_config" do
159
+ expect(LogStash::Config::Loader).to receive(:new).with(anything, true).and_call_original
160
+ expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:debug_config => true)).and_return(pipeline)
161
+ args = ["--debug", "--debug-config", "-e", pipeline_string]
162
+ subject.run("bin/logstash", args)
163
+ end
164
+ end
165
+
166
+ context "when configuring environment variable support" do
167
+ it "should set 'allow_env' to false by default" do
168
+ args = ["-e", pipeline_string]
169
+ expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:allow_env => false)).and_return(pipeline)
170
+ subject.run("bin/logstash", args)
171
+ end
172
+
173
+ it "should support templating environment variables" do
174
+ args = ["-e", pipeline_string, "--allow-env"]
175
+ expect(LogStash::Pipeline).to receive(:new).with(pipeline_string, hash_including(:allow_env => true)).and_return(pipeline)
176
+ subject.run("bin/logstash", args)
81
177
  end
82
178
  end
83
179
  end
@@ -20,7 +20,6 @@ describe LogStash::ShutdownWatcher do
20
20
  allow(pipeline).to receive(:thread).and_return(Thread.current)
21
21
  allow(reporter).to receive(:snapshot).and_return(reporter_snapshot)
22
22
  allow(reporter_snapshot).to receive(:o_simple_hash).and_return({})
23
- allow(reporter_snapshot).to receive(:to_json_data).and_return("reporter-double")
24
23
 
25
24
  allow(subject).to receive(:pipeline_report_snapshot).and_wrap_original do |m, *args|
26
25
  report_count += 1
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ require "logstash/util/duration_formatter"
3
+ require "spec_helper"
4
+
5
+ describe LogStash::Util::DurationFormatter do
6
+ let(:duration) { 3600 * 1000 } # in milliseconds
7
+
8
+ it "returns a human format" do
9
+ expect(subject.human_format(duration)).to eq("1h")
10
+ end
11
+ end
@@ -19,10 +19,18 @@ describe "LogStash::Util::JavaVersion" do
19
19
  expect(mod.bad_java_version?("1.6.0")).to be_truthy
20
20
  end
21
21
 
22
+ it "should mark java 7 version as bad" do
23
+ expect(mod.bad_java_version?("1.7.0_51")).to be_truthy
24
+ end
25
+
26
+ it "should mark java version 8 as good" do
27
+ expect(mod.bad_java_version?("1.8.0")).to be_falsey
28
+ end
29
+
22
30
  it "should mark a good standard java version as good" do
23
- expect(mod.bad_java_version?("1.7.0_51")).to be_falsey
31
+ expect(mod.bad_java_version?("1.8.0_65")).to be_falsey
24
32
  end
25
-
33
+
26
34
  it "should mark a good beta version as good" do
27
35
  expect(mod.bad_java_version?("1.8.0-beta")).to be_falsey
28
36
  end
@@ -3,8 +3,18 @@ require 'spec_helper'
3
3
 
4
4
  require "logstash/util"
5
5
 
6
+ class ClassNameTest
7
+ end
8
+
9
+ module TestingClassName
10
+ class TestKlass
11
+ end
12
+ end
13
+
6
14
  describe LogStash::Util do
7
15
 
16
+ subject { described_class }
17
+
8
18
  context "stringify_keys" do
9
19
  it "should convert hash symbol keys to strings" do
10
20
  expect(LogStash::Util.stringify_symbols({:a => 1, "b" => 2})).to eq({"a" => 1, "b" => 2})
@@ -32,4 +42,22 @@ describe LogStash::Util do
32
42
  expect(LogStash::Util.stringify_symbols([:a, [1, :b]])).to eq(["a", [1, "b"]])
33
43
  end
34
44
  end
45
+
46
+ describe ".class_name" do
47
+ context "when the class is a top level class" do
48
+ let(:klass) { ClassNameTest.new }
49
+
50
+ it "returns the name of the class" do
51
+ expect(subject.class_name(klass)).to eq("ClassNameTest")
52
+ end
53
+ end
54
+
55
+ context "when the class is nested inside modules" do
56
+ let(:klass) { TestingClassName::TestKlass.new }
57
+
58
+ it "returns the name of the class" do
59
+ expect(subject.class_name(klass)).to eq("TestKlass")
60
+ end
61
+ end
62
+ end
35
63
  end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ require "rspec"
3
+ require "rspec/expectations"
4
+
5
+ RSpec::Matchers.define :be_a_metric_event do |namespace, type, *args|
6
+ match do
7
+ namespace == Array(actual[0]).concat(Array(actual[1])) &&
8
+ type == actual[2] &&
9
+ args == actual[3..-1]
10
+ end
11
+ end
12
+
13
+ # Match to test `NullObject` pattern
14
+ RSpec::Matchers.define :implement_interface_of do |type, key, value|
15
+ match do |actual|
16
+ all_instance_methods_implemented?
17
+ end
18
+
19
+ def missing_methods
20
+ expected.instance_methods.select { |method| !actual.instance_methods.include?(method) }
21
+ end
22
+
23
+ def all_instance_methods_implemented?
24
+ expected.instance_methods.all? { |method| actual.instance_methods.include?(method) }
25
+ end
26
+
27
+ failure_message do
28
+ "Expecting `#{expected}` to implements instance methods of `#{actual}`, missing methods: #{missing_methods.join(",")}"
29
+ end
30
+ end