fluentd 1.13.1 → 1.14.0

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

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +69 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yaml +38 -0
  4. data/.github/workflows/windows-test.yaml +3 -3
  5. data/CHANGELOG.md +105 -0
  6. data/README.md +2 -2
  7. data/example/v0_12_filter.conf +2 -2
  8. data/fluentd.gemspec +1 -1
  9. data/lib/fluent/command/fluentd.rb +8 -0
  10. data/lib/fluent/command/plugin_generator.rb +15 -5
  11. data/lib/fluent/compat/output.rb +9 -6
  12. data/lib/fluent/config/types.rb +15 -0
  13. data/lib/fluent/config/v1_parser.rb +3 -2
  14. data/lib/fluent/config.rb +1 -1
  15. data/lib/fluent/env.rb +2 -1
  16. data/lib/fluent/event_router.rb +28 -1
  17. data/lib/fluent/oj_options.rb +62 -0
  18. data/lib/fluent/plugin/bare_output.rb +49 -8
  19. data/lib/fluent/plugin/buffer.rb +84 -22
  20. data/lib/fluent/plugin/file_wrapper.rb +22 -0
  21. data/lib/fluent/plugin/filter.rb +35 -1
  22. data/lib/fluent/plugin/formatter.rb +1 -0
  23. data/lib/fluent/plugin/formatter_json.rb +9 -7
  24. data/lib/fluent/plugin/in_http.rb +21 -2
  25. data/lib/fluent/plugin/in_monitor_agent.rb +4 -2
  26. data/lib/fluent/plugin/in_syslog.rb +13 -1
  27. data/lib/fluent/plugin/in_tail/position_file.rb +20 -18
  28. data/lib/fluent/plugin/in_tail.rb +45 -3
  29. data/lib/fluent/plugin/input.rb +39 -1
  30. data/lib/fluent/plugin/metrics.rb +119 -0
  31. data/lib/fluent/plugin/metrics_local.rb +96 -0
  32. data/lib/fluent/plugin/multi_output.rb +43 -6
  33. data/lib/fluent/plugin/output.rb +74 -33
  34. data/lib/fluent/plugin/parser_json.rb +2 -3
  35. data/lib/fluent/plugin.rb +10 -1
  36. data/lib/fluent/plugin_helper/event_emitter.rb +8 -1
  37. data/lib/fluent/plugin_helper/metrics.rb +129 -0
  38. data/lib/fluent/plugin_helper/server.rb +4 -2
  39. data/lib/fluent/plugin_helper.rb +1 -0
  40. data/lib/fluent/root_agent.rb +6 -0
  41. data/lib/fluent/supervisor.rb +2 -0
  42. data/lib/fluent/system_config.rb +9 -1
  43. data/lib/fluent/test/driver/storage.rb +30 -0
  44. data/lib/fluent/version.rb +1 -1
  45. data/templates/new_gem/lib/fluent/plugin/storage.rb.erb +40 -0
  46. data/templates/new_gem/test/plugin/test_storage.rb.erb +18 -0
  47. data/test/command/test_plugin_generator.rb +2 -1
  48. data/test/config/test_system_config.rb +6 -0
  49. data/test/config/test_types.rb +7 -0
  50. data/test/plugin/in_tail/test_position_file.rb +48 -8
  51. data/test/plugin/test_bare_output.rb +13 -0
  52. data/test/plugin/test_buffer.rb +8 -2
  53. data/test/plugin/test_file_wrapper.rb +11 -0
  54. data/test/plugin/test_filter.rb +11 -0
  55. data/test/plugin/test_in_http.rb +40 -0
  56. data/test/plugin/test_in_monitor_agent.rb +214 -8
  57. data/test/plugin/test_in_syslog.rb +35 -0
  58. data/test/plugin/test_in_tail.rb +138 -26
  59. data/test/plugin/test_input.rb +11 -0
  60. data/test/plugin/test_metrics.rb +294 -0
  61. data/test/plugin/test_metrics_local.rb +96 -0
  62. data/test/plugin/test_multi_output.rb +25 -1
  63. data/test/plugin/test_output.rb +16 -0
  64. data/test/plugin_helper/test_event_emitter.rb +29 -0
  65. data/test/plugin_helper/test_metrics.rb +137 -0
  66. data/test/test_event_time.rb +2 -2
  67. data/test/test_oj_options.rb +55 -0
  68. data/test/test_plugin_classes.rb +102 -0
  69. data/test/test_root_agent.rb +30 -1
  70. metadata +21 -6
  71. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  72. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
@@ -467,4 +467,39 @@ EOS
467
467
  assert_equal tests.size, d.events.size
468
468
  compare_unmatched_lines_test_result(d.events, tests, {address: address})
469
469
  end
470
+
471
+ def test_send_keepalive_packet_is_disabled_by_default
472
+ d = create_driver(ipv4_config + %[
473
+ <transport tcp>
474
+ </transport>
475
+ protocol tcp
476
+ ])
477
+ assert_false d.instance.send_keepalive_packet
478
+ end
479
+
480
+ def test_send_keepalive_packet_can_be_enabled
481
+ addr = "127.0.0.1"
482
+ d = create_driver(ipv4_config + %[
483
+ <transport tcp>
484
+ </transport>
485
+ send_keepalive_packet true
486
+ ])
487
+ assert_true d.instance.send_keepalive_packet
488
+ mock.proxy(d.instance).server_create_connection(
489
+ :in_syslog_tcp_server, @port,
490
+ bind: addr,
491
+ resolve_name: nil,
492
+ send_keepalive_packet: true)
493
+ d.run do
494
+ TCPSocket.open(addr, @port)
495
+ end
496
+ end
497
+
498
+ def test_send_keepalive_packet_can_not_be_enabled_for_udp
499
+ assert_raise(Fluent::ConfigError) do
500
+ d = create_driver(ipv4_config + %[
501
+ send_keepalive_packet true
502
+ ])
503
+ end
504
+ end
470
505
  end
@@ -109,6 +109,7 @@ class TailInputTest < Test::Unit::TestCase
109
109
  "refresh_interval" => "1s",
110
110
  "read_from_head" => "true",
111
111
  "format" => "none",
112
+ "rotate_wait" => "1s",
112
113
  "follow_inodes" => "true"
113
114
  })
114
115
  SINGLE_LINE_CONFIG = config_element("", "", { "format" => "/(?<message>.*)/" })
@@ -456,6 +457,91 @@ class TailInputTest < Test::Unit::TestCase
456
457
  assert_equal([], d.events)
457
458
  end
458
459
  end
460
+
461
+ sub_test_case "EOF with reads_bytes_per_second" do
462
+ def test_longer_than_rotate_wait
463
+ limit_bytes = 8192
464
+ num_lines = 1024 * 3
465
+ msg = "08bytes"
466
+
467
+ File.open("#{TMP_DIR}/tail.txt", "wb") do |f|
468
+ f.write("#{msg}\n" * num_lines)
469
+ end
470
+
471
+ config = CONFIG_READ_FROM_HEAD +
472
+ SINGLE_LINE_CONFIG +
473
+ config_element("", "", {
474
+ "read_bytes_limit_per_second" => limit_bytes,
475
+ "rotate_wait" => 0.1,
476
+ "refresh_interval" => 0.5,
477
+ })
478
+
479
+ rotated = false
480
+ d = create_driver(config)
481
+ d.run(timeout: 10) do
482
+ while d.events.size < num_lines do
483
+ if d.events.size > 0 && !rotated
484
+ cleanup_file("#{TMP_DIR}/tail.txt")
485
+ FileUtils.touch("#{TMP_DIR}/tail.txt")
486
+ rotated = true
487
+ end
488
+ sleep 0.3
489
+ end
490
+ end
491
+
492
+ assert_equal(num_lines,
493
+ d.events.count do |event|
494
+ event[2]["message"] == msg
495
+ end)
496
+ end
497
+
498
+ def test_shorter_than_rotate_wait
499
+ limit_bytes = 8192
500
+ num_lines = 1024 * 2
501
+ msg = "08bytes"
502
+
503
+ File.open("#{TMP_DIR}/tail.txt", "wb") do |f|
504
+ f.write("#{msg}\n" * num_lines)
505
+ end
506
+
507
+ config = CONFIG_READ_FROM_HEAD +
508
+ SINGLE_LINE_CONFIG +
509
+ config_element("", "", {
510
+ "read_bytes_limit_per_second" => limit_bytes,
511
+ "rotate_wait" => 2,
512
+ "refresh_interval" => 0.5,
513
+ })
514
+
515
+ start_time = Fluent::Clock.now
516
+ rotated = false
517
+ detached = false
518
+ d = create_driver(config)
519
+ mock.proxy(d.instance).setup_watcher(anything, anything) do |tw|
520
+ mock.proxy(tw).detach(anything) do |v|
521
+ detached = true
522
+ v
523
+ end
524
+ tw
525
+ end.twice
526
+
527
+ d.run(timeout: 10) do
528
+ until detached do
529
+ if d.events.size > 0 && !rotated
530
+ cleanup_file("#{TMP_DIR}/tail.txt")
531
+ FileUtils.touch("#{TMP_DIR}/tail.txt")
532
+ rotated = true
533
+ end
534
+ sleep 0.3
535
+ end
536
+ end
537
+
538
+ assert_true(Fluent::Clock.now - start_time > 2)
539
+ assert_equal(num_lines,
540
+ d.events.count do |event|
541
+ event[2]["message"] == msg
542
+ end)
543
+ end
544
+ end
459
545
  end
460
546
 
461
547
  data(flat: CONFIG_READ_FROM_HEAD + SINGLE_LINE_CONFIG,
@@ -1653,7 +1739,7 @@ class TailInputTest < Test::Unit::TestCase
1653
1739
  d.instance_shutdown
1654
1740
  end
1655
1741
 
1656
- def test_should_keep_and_update_existing_file_pos_entry_for_deleted_file_when_new_file_with_same_name_created
1742
+ def test_should_remove_deleted_file
1657
1743
  config = config_element("", "", {"format" => "none"})
1658
1744
 
1659
1745
  path = "#{TMP_DIR}/tail.txt"
@@ -1664,30 +1750,11 @@ class TailInputTest < Test::Unit::TestCase
1664
1750
  }
1665
1751
 
1666
1752
  d = create_driver(config)
1667
- d.run(shutdown: false)
1668
-
1669
- pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1670
- pos_file.pos = 0
1671
-
1672
- path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1673
- assert_equal(path, path_pos_ino[1])
1674
- assert_equal(pos, path_pos_ino[2].to_i(16))
1675
- assert_equal(ino, path_pos_ino[3].to_i(16))
1676
-
1677
- File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1678
- f.puts "test1"
1679
- f.puts "test2"
1680
- }
1681
- Timecop.travel(Time.now + 10) do
1682
- sleep 5
1753
+ d.run do
1754
+ pos_file = File.open("#{TMP_DIR}/tail.pos", "r")
1683
1755
  pos_file.pos = 0
1684
- tuple = create_target_info("#{TMP_DIR}/tail.txt")
1685
- path_pos_ino = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(pos_file.readline)
1686
- assert_equal(tuple.path, path_pos_ino[1])
1687
- assert_equal(12, path_pos_ino[2].to_i(16))
1688
- assert_equal(tuple.ino, path_pos_ino[3].to_i(16))
1756
+ assert_equal([], pos_file.readlines)
1689
1757
  end
1690
- d.instance_shutdown
1691
1758
  end
1692
1759
 
1693
1760
  def test_should_mark_file_unwatched_after_limit_recently_modified_and_rotate_wait
@@ -1899,6 +1966,49 @@ class TailInputTest < Test::Unit::TestCase
1899
1966
  assert_equal({"message" => "test4"}, events[3][2])
1900
1967
  d.instance_shutdown
1901
1968
  end
1969
+
1970
+ # issue #3464
1971
+ def test_should_replace_target_info
1972
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f|
1973
+ f.puts "test1\n"
1974
+ }
1975
+ target_info = create_target_info("#{TMP_DIR}/tail.txt")
1976
+ inodes = []
1977
+
1978
+ config = config_element("ROOT", "", {
1979
+ "path" => "#{TMP_DIR}/tail.txt*",
1980
+ "pos_file" => "#{TMP_DIR}/tail.pos",
1981
+ "tag" => "t1",
1982
+ "refresh_interval" => "60s",
1983
+ "read_from_head" => "true",
1984
+ "format" => "none",
1985
+ "rotate_wait" => "1s",
1986
+ "follow_inodes" => "true"
1987
+ })
1988
+ d = create_driver(config, false)
1989
+ d.run(timeout: 5) do
1990
+ while d.events.size < 1 do
1991
+ sleep 0.1
1992
+ end
1993
+ inodes = d.instance.instance_variable_get(:@tails).keys.collect do |key|
1994
+ key.ino
1995
+ end
1996
+ assert_equal([target_info.ino], inodes)
1997
+
1998
+ cleanup_file("#{TMP_DIR}/tail.txt")
1999
+ File.open("#{TMP_DIR}/tail.txt", "wb") {|f| f.puts "test2\n"}
2000
+
2001
+ while d.events.size < 2 do
2002
+ sleep 0.1
2003
+ end
2004
+ inodes = d.instance.instance_variable_get(:@tails).keys.collect do |key|
2005
+ key.ino
2006
+ end
2007
+ new_target_info = create_target_info("#{TMP_DIR}/tail.txt")
2008
+ assert_not_equal(target_info.ino, new_target_info.ino)
2009
+ assert_equal([new_target_info.ino], inodes)
2010
+ end
2011
+ end
1902
2012
  end
1903
2013
 
1904
2014
  sub_test_case "tail_path" do
@@ -2069,8 +2179,9 @@ class TailInputTest < Test::Unit::TestCase
2069
2179
  assert_nothing_raised do
2070
2180
  d.run(shutdown: false) {}
2071
2181
  end
2072
- d.instance_shutdown
2073
2182
  assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed with Errno::ENOENT. Drop tail watcher for now.\n") })
2183
+ ensure
2184
+ d.instance_shutdown if d && d.instance
2074
2185
  end
2075
2186
 
2076
2187
  def test_EACCES_error_after_setup_watcher
@@ -2093,10 +2204,10 @@ class TailInputTest < Test::Unit::TestCase
2093
2204
  assert_nothing_raised do
2094
2205
  d.run(shutdown: false) {}
2095
2206
  end
2096
- d.instance_shutdown
2097
2207
  assert($log.out.logs.any?{|log| log.include?("stat() for #{path} failed with Errno::EACCES. Drop tail watcher for now.\n") })
2098
2208
  end
2099
2209
  ensure
2210
+ d.instance_shutdown if d && d.instance
2100
2211
  if File.exist?("#{TMP_DIR}/noaccess")
2101
2212
  FileUtils.chmod(0755, "#{TMP_DIR}/noaccess")
2102
2213
  FileUtils.rm_rf("#{TMP_DIR}/noaccess")
@@ -2116,8 +2227,9 @@ class TailInputTest < Test::Unit::TestCase
2116
2227
  assert_nothing_raised do
2117
2228
  d.run(shutdown: false) {}
2118
2229
  end
2119
- d.instance_shutdown
2120
2230
  assert($log.out.logs.any?{|log| log.include?("expand_paths: stat() for #{path} failed with Errno::EACCES. Skip file.\n") })
2231
+ ensure
2232
+ d.instance_shutdown if d && d.instance
2121
2233
  end
2122
2234
 
2123
2235
  def test_shutdown_timeout
@@ -85,6 +85,17 @@ class InputTest < Test::Unit::TestCase
85
85
  end
86
86
  end
87
87
 
88
+ test 'can use metrics plugins and fallback methods' do
89
+ @p.configure(config_element('ROOT', '', {'@log_level' => 'debug'}))
90
+
91
+ %w[emit_size_metrics emit_records_metrics].each do |metric_name|
92
+ assert_true @p.instance_variable_get(:"@#{metric_name}").is_a?(Fluent::Plugin::Metrics)
93
+ end
94
+
95
+ assert_equal 0, @p.emit_size
96
+ assert_equal 0, @p.emit_records
97
+ end
98
+
88
99
  test 'are not available with multi workers configuration in default' do
89
100
  assert_false @p.multi_workers_ready?
90
101
  end
@@ -0,0 +1,294 @@
1
+ require_relative '../helper'
2
+ require 'fluent/plugin/metrics'
3
+ require 'fluent/plugin/base'
4
+ require 'fluent/system_config'
5
+
6
+ class BareMetrics < Fluent::Plugin::Metrics
7
+ Fluent::Plugin.register_metrics('bare', self)
8
+
9
+ private
10
+
11
+ # Just override for tests.
12
+ def has_methods_for_counter?
13
+ false
14
+ end
15
+ end
16
+
17
+ class BasicCounterMetrics < Fluent::Plugin::Metrics
18
+ Fluent::Plugin.register_metrics('example', self)
19
+
20
+ attr_reader :data
21
+
22
+ def initialize
23
+ super
24
+ @data = 0
25
+ end
26
+ def get
27
+ @data
28
+ end
29
+ def inc
30
+ @data +=1
31
+ end
32
+ def add(value)
33
+ @data += value
34
+ end
35
+ def set(value)
36
+ @data = value
37
+ end
38
+ def close
39
+ @data = 0
40
+ super
41
+ end
42
+ end
43
+
44
+ class AliasedCounterMetrics < Fluent::Plugin::Metrics
45
+ Fluent::Plugin.register_metrics('example', self)
46
+
47
+ attr_reader :data
48
+
49
+ def initialize
50
+ super
51
+ @data = 0
52
+ end
53
+ def configure(conf)
54
+ super
55
+ class << self
56
+ alias_method :set, :set_counter
57
+ end
58
+ end
59
+ def get
60
+ @data
61
+ end
62
+ def inc
63
+ @data +=1
64
+ end
65
+ def add(value)
66
+ @data += value
67
+ end
68
+ def set_counter(value)
69
+ @data = value
70
+ end
71
+ def close
72
+ @data = 0
73
+ super
74
+ end
75
+ end
76
+
77
+ class BasicGaugeMetrics < Fluent::Plugin::Metrics
78
+ Fluent::Plugin.register_metrics('example', self)
79
+
80
+ attr_reader :data
81
+
82
+ def initialize
83
+ super
84
+ @data = 0
85
+ end
86
+ def get
87
+ @data
88
+ end
89
+ def inc
90
+ @data +=1
91
+ end
92
+ def dec
93
+ @data -=1
94
+ end
95
+ def add(value)
96
+ @data += value
97
+ end
98
+ def sub(value)
99
+ @data -= value
100
+ end
101
+ def set(value)
102
+ @data = value
103
+ end
104
+ def close
105
+ @data = 0
106
+ super
107
+ end
108
+ end
109
+
110
+ class AliasedGaugeMetrics < Fluent::Plugin::Metrics
111
+ Fluent::Plugin.register_metrics('example', self)
112
+
113
+ attr_reader :data
114
+
115
+ def initialize
116
+ super
117
+ @data = 0
118
+ end
119
+ def configure(conf)
120
+ super
121
+ class << self
122
+ alias_method :dec, :dec_gauge
123
+ alias_method :set, :set_gauge
124
+ alias_method :sub, :sub_gauge
125
+ end
126
+ end
127
+ def get
128
+ @data
129
+ end
130
+ def inc
131
+ @data +=1
132
+ end
133
+ def dec_gauge
134
+ @data -=1
135
+ end
136
+ def add(value)
137
+ @data += value
138
+ end
139
+ def sub_gauge(value)
140
+ @data -= value
141
+ end
142
+ def set_gauge(value)
143
+ @data = value
144
+ end
145
+ def close
146
+ @data = 0
147
+ super
148
+ end
149
+ end
150
+
151
+ class StorageTest < Test::Unit::TestCase
152
+ sub_test_case 'BareMetrics' do
153
+ setup do
154
+ @m = BareMetrics.new
155
+ @m.configure(config_element())
156
+ end
157
+
158
+ test 'is configured with plugin information and system config' do
159
+ m = BareMetrics.new
160
+ m.configure(config_element('metrics', '', {}))
161
+
162
+ assert_false m.use_gauge_metric
163
+ assert_false m.has_methods_for_counter
164
+ assert_false m.has_methods_for_gauge
165
+ end
166
+
167
+ test 'all bare operations are not defined yet' do
168
+ assert_raise NotImplementedError do
169
+ @m.get
170
+ end
171
+ assert_raise NotImplementedError do
172
+ @m.inc
173
+ end
174
+ assert_raise NotImplementedError do
175
+ @m.dec
176
+ end
177
+ assert_raise NotImplementedError do
178
+ @m.add(10)
179
+ end
180
+ assert_raise NotImplementedError do
181
+ @m.sub(11)
182
+ end
183
+ assert_raise NotImplementedError do
184
+ @m.set(123)
185
+ end
186
+ end
187
+ end
188
+
189
+ sub_test_case 'BasicCounterMetric' do
190
+ setup do
191
+ @m = BasicCounterMetrics.new
192
+ @m.configure(config_element('metrics', '', {'@id' => '1'}))
193
+ end
194
+
195
+ test 'all basic counter operations work well' do
196
+ assert_true @m.has_methods_for_counter
197
+ assert_false @m.has_methods_for_gauge
198
+
199
+ assert_equal 0, @m.get
200
+ assert_equal 1, @m.inc
201
+
202
+ @m.add(20)
203
+ assert_equal 21, @m.get
204
+ assert_raise NotImplementedError do
205
+ @m.dec
206
+ end
207
+
208
+ @m.set(100)
209
+ assert_equal 100, @m.get
210
+ assert_raise NotImplementedError do
211
+ @m.sub(11)
212
+ end
213
+ end
214
+ end
215
+
216
+ sub_test_case 'AliasedCounterMetric' do
217
+ setup do
218
+ @m = AliasedCounterMetrics.new
219
+ @m.configure(config_element('metrics', '', {}))
220
+ end
221
+
222
+ test 'all aliased counter operations work well' do
223
+ assert_true @m.has_methods_for_counter
224
+ assert_false @m.has_methods_for_gauge
225
+
226
+ assert_equal 0, @m.get
227
+ assert_equal 1, @m.inc
228
+
229
+ @m.add(20)
230
+ assert_equal 21, @m.get
231
+ assert_raise NotImplementedError do
232
+ @m.dec
233
+ end
234
+
235
+ @m.set(100)
236
+ assert_equal 100, @m.get
237
+ assert_raise NotImplementedError do
238
+ @m.sub(11)
239
+ end
240
+ end
241
+ end
242
+
243
+ sub_test_case 'BasicGaugeMetric' do
244
+ setup do
245
+ @m = BasicGaugeMetrics.new
246
+ @m.use_gauge_metric = true
247
+ @m.configure(config_element('metrics', '', {}))
248
+ end
249
+
250
+ test 'all basic gauge operations work well' do
251
+ assert_false @m.has_methods_for_counter
252
+ assert_true @m.has_methods_for_gauge
253
+
254
+ assert_equal 0, @m.get
255
+ assert_equal 1, @m.inc
256
+
257
+ @m.add(20)
258
+ assert_equal 21, @m.get
259
+ @m.dec
260
+ assert_equal 20, @m.get
261
+
262
+ @m.set(100)
263
+ assert_equal 100, @m.get
264
+ @m.sub(11)
265
+ assert_equal 89, @m.get
266
+ end
267
+ end
268
+
269
+ sub_test_case 'AliasedGaugeMetric' do
270
+ setup do
271
+ @m = AliasedGaugeMetrics.new
272
+ @m.use_gauge_metric = true
273
+ @m.configure(config_element('metrics', '', {}))
274
+ end
275
+
276
+ test 'all aliased gauge operations work well' do
277
+ assert_false @m.has_methods_for_counter
278
+ assert_true @m.has_methods_for_gauge
279
+
280
+ assert_equal 0, @m.get
281
+ assert_equal 1, @m.inc
282
+
283
+ @m.add(20)
284
+ assert_equal 21, @m.get
285
+ @m.dec
286
+ assert_equal 20, @m.get
287
+
288
+ @m.set(100)
289
+ assert_equal 100, @m.get
290
+ @m.sub(11)
291
+ assert_equal 89, @m.get
292
+ end
293
+ end
294
+ end