yabeda 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5aed70338bdfe24c4505a3cbe51cb4853c7c0e0c1842ef1cee93480ef517c02
4
- data.tar.gz: 95dc76b40980f227abe24c7bf70339d47d81eaa91e6bdfa354c8b6e6b93c8170
3
+ metadata.gz: 262fe8c41ef493e3792e67b3e569f3820f8fc8de11a5795807d8d81efe8e0d9f
4
+ data.tar.gz: d86b4a968f1894ce65af4976873135957a122672cd58453023d38792b60c4db7
5
5
  SHA512:
6
- metadata.gz: 77e865d851f2e9f2a168426983df6693c9e655e0349107aafd25594eea12f9f83c14da519abbdfc5826f8c0e208d68b2a0616735771ba5ec82bde7acb3e71085
7
- data.tar.gz: aa7596a3de80b063845d552136c882cbdf46e772a1f53998058ba894dc8428290fdbae391ef241f2264dd5e76bd41be8cfab0b8afad654152b50d3b47fd0ab3b
6
+ metadata.gz: 22c41d84fb0efbb8ca1802e02c1c4e6636fc8a2ae05c22775d4390ddc5ddfd0926b97464d6b2702e6b82a1b8709d66bf391bcb7aa6ed05df9a640a0143817889
7
+ data.tar.gz: ad5e58fc8b678c0764b6d8f3677c56a08d087dcbc9407b3a6864fa0e85226a773dce2efaa365d8bed0d0c25db300829811688819a407dee86bcefbea439e2232
data/.rubocop.yml CHANGED
@@ -23,6 +23,12 @@ RSpec/LetSetup:
23
23
  RSpec/MultipleExpectations:
24
24
  Enabled: false
25
25
 
26
+ RSpec/DescribeClass:
27
+ Enabled: false
28
+
29
+ RSpec/NestedGroups:
30
+ Max: 4
31
+
26
32
  Bundler/OrderedGems:
27
33
  Enabled: false
28
34
 
@@ -52,3 +58,6 @@ Style/HashTransformKeys:
52
58
 
53
59
  Style/HashTransformValues:
54
60
  Enabled: true
61
+
62
+ Style/Documentation:
63
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.11.0 - 2021-09-25
11
+
12
+ ### Added
13
+
14
+ - RSpec matchers `increment_yabeda_counter`, `update_yabeda_gauge`, and `measure_yabeda_histogram` for convenient testing. [#25](https://github.com/yabeda-rb/yabeda/pull/25) by [@Envek][]
15
+ - Automatic setup of RSpec on `require "yabeda/rspec"`
16
+ - Special test adapter that collects metric changes in memory
17
+
10
18
  ## 0.10.1 - 2021-08-30
11
19
 
12
20
  ### Fixed
data/README.md CHANGED
@@ -162,6 +162,44 @@ Config key | Type | Default | Description |
162
162
 
163
163
  These are only enabled in debug mode. To enable it either set `debug` config key to `true` (e.g. by specifying `YABEDA_DEBUG=true` in your environment variables or executing `Yabeda.debug!` in your code).
164
164
 
165
+ ## Testing
166
+
167
+ ### RSpec
168
+
169
+ Add the following to your `rails_helper.rb` (or `spec_helper.rb`):
170
+
171
+ ```ruby
172
+ require "yabeda/rspec"
173
+ ```
174
+
175
+ Now you can use `increment_yabeda_counter`, `update_yabeda_gauge`, and `measure_yabeda_histogram` matchers:
176
+
177
+ ```ruby
178
+ it "increments counters" do
179
+ expect { subject }.to increment_yabeda_counter(Yabeda.myapp.foo_count).by(3)
180
+ end
181
+ ```
182
+
183
+ You can scope metrics by used tags with `with_tags`:
184
+
185
+ ```ruby
186
+ it "updates gauges" do
187
+ expect { subject }.to \
188
+ update_yabeda_gauge("some_gauge_name").
189
+ with_tags(method: "command", command: "subscribe")
190
+ end
191
+ ```
192
+
193
+ Note that tags you specified doesn't need to be exact, but can be a subset of tags used on metric update. In this example updates with following sets of tags `{ method: "command", command: "subscribe", status: "SUCCESS" }` and `{ method: "command", command: "subscribe", status: "FAILURE" }` will make test example to pass.
194
+
195
+ And check for values with `by` for counters, `to` for gauges, and `with` for gauges and histograms (and you [can use other matchers here](https://relishapp.com/rspec/rspec-expectations/v/3-10/docs/composing-matchers)):
196
+
197
+ ```ruby
198
+ expect { subject }.to \
199
+ measure_yabeda_histogram(Yabeda.something.anything_runtime).
200
+ with(be_between(0.005, 0.05))
201
+ ```
202
+
165
203
  ## Roadmap (aka TODO or Help wanted)
166
204
 
167
205
  - Ability to change metric settings for individual adapters
@@ -11,7 +11,6 @@ require "yabeda/dsl/metric_builder"
11
11
  module Yabeda
12
12
  # DSL for ease of work with Yabeda
13
13
  module DSL
14
- # rubocop: disable Style/Documentation
15
14
  module ClassMethods
16
15
  # Block for grouping and simplifying configuration of related metrics
17
16
  def configure(&block)
@@ -110,6 +109,5 @@ module Yabeda
110
109
  group.register_metric(metric)
111
110
  end
112
111
  end
113
- # rubocop: enable Style/Documentation
114
112
  end
115
113
  end
data/lib/yabeda/metric.rb CHANGED
@@ -29,5 +29,9 @@ module Yabeda
29
29
  def tags
30
30
  (Yabeda.groups[group].default_tags.keys + Array(super)).uniq
31
31
  end
32
+
33
+ def inspect
34
+ "#<#{self.class.name}: #{[@group, @name].compact.join('.')}>"
35
+ end
32
36
  end
33
37
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yabeda
4
+ module RSpec
5
+ # Notes:
6
+ # +expected+ is always a metric instance
7
+ # +actual+ is always a block of code
8
+ # Example:
9
+ # expect { anything }.to do_whatever_with_yabeda_metric(Yabeda.something)
10
+ class BaseMatcher < ::RSpec::Matchers::BuiltIn::BaseMatcher
11
+ attr_reader :tags, :metric
12
+
13
+ # Specify a scope of labels (tags). Subset of tags can be specified.
14
+ def with_tags(tags)
15
+ @tags = tags
16
+ self
17
+ end
18
+
19
+ def initialize(expected)
20
+ @expected = @metric = resolve_metric(expected)
21
+ rescue KeyError
22
+ raise ArgumentError, <<~MSG
23
+ Pass metric name or metric instance to matcher (e.g. `increment_yabeda_counter(Yabeda.metric_name)` or \
24
+ increment_yabeda_counter('metric_name')). Got #{expected.inspect} instead
25
+ MSG
26
+ end
27
+
28
+ # RSpec doesn't define this method, but it is more convenient to rely on +match_when_negated+ method presence
29
+ def does_not_match?(actual)
30
+ @actual = actual
31
+ if respond_to?(:match_when_negated)
32
+ match_when_negated(expected, actual)
33
+ else
34
+ !match(expected, actual)
35
+ end
36
+ end
37
+
38
+ def supports_block_expectations?
39
+ true
40
+ end
41
+
42
+ # Pretty print metric name (expected is expected to always be a Yabeda metric instance)
43
+ def expected_formatted
44
+ "Yabeda.#{[metric.group, metric.name].compact.join('.')}"
45
+ end
46
+
47
+ private
48
+
49
+ def resolve_metric(instance_or_name)
50
+ return instance_or_name if instance_or_name.is_a? Yabeda::Metric
51
+
52
+ Yabeda.metrics.fetch(instance_or_name.to_s)
53
+ end
54
+
55
+ # Filter metric changes by tags.
56
+ # If tags specified, treat them as subset of real tags (to avoid bothering with default tags in tests)
57
+ def filter_matching_changes(changes)
58
+ return changes if tags.nil?
59
+
60
+ changes.select { |t, _v| t >= tags }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda counter was incremented during test run or not
8
+ # @param metric [Yabeda::Counter,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::IncrementYabedaCounter]
10
+ def increment_yabeda_counter(metric)
11
+ IncrementYabedaCounter.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +increment_yabeda_counter+
15
+ class IncrementYabedaCounter < BaseMatcher
16
+ def by(increment)
17
+ @expected_increment = increment
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_increment
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Counter
26
+
27
+ raise ArgumentError, "Pass counter instance/name to `increment_yabeda_counter`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ increments = filter_matching_changes(Yabeda::TestAdapter.instance.counters.fetch(metric))
34
+
35
+ increments.values.any? do |actual_increment|
36
+ expected_increment.nil? || values_match?(expected_increment, actual_increment)
37
+ end
38
+ end
39
+
40
+ def match_when_negated(metric, block)
41
+ unless expected_increment.nil?
42
+ raise NotImplementedError, <<~MSG
43
+ `expect(Yabeda.metric_name).not_to increment_yabeda_counter` doesn't support specifying increment
44
+ with `.by` as it can lead to false positives.
45
+ MSG
46
+ end
47
+
48
+ block.call
49
+
50
+ increments = filter_matching_changes(Yabeda::TestAdapter.instance.counters.fetch(metric))
51
+
52
+ increments.none?
53
+ end
54
+
55
+ def failure_message
56
+ "expected #{expected_formatted} " \
57
+ "to be incremented #{"by #{description_of(expected_increment)} " unless expected_increment.nil?}" \
58
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
59
+ "but #{actual_increments_message}"
60
+ end
61
+
62
+ def failure_message_when_negated
63
+ "expected #{expected_formatted} " \
64
+ "not to be incremented " \
65
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
66
+ "but #{actual_increments_message}"
67
+ end
68
+
69
+ def actual_increments_message
70
+ counter_increments = Yabeda::TestAdapter.instance.counters.fetch(metric)
71
+ if counter_increments.empty?
72
+ "no increments of this counter have been made"
73
+ elsif tags && counter_increments.key?(tags)
74
+ "has been incremented by #{counter_increments.fetch(tags)}"
75
+ else
76
+ "following increments have been made: #{::RSpec::Support::ObjectFormatter.format(counter_increments)}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda histogram was measured during test run or not
8
+ # @param metric [Yabeda::Histogram,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::MeasureYabedaHistogram]
10
+ def measure_yabeda_histogram(metric)
11
+ MeasureYabedaHistogram.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +measure_yabeda_histogram+
15
+ class MeasureYabedaHistogram < BaseMatcher
16
+ def with(value)
17
+ @expected_value = value
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_value
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Histogram
26
+
27
+ raise ArgumentError, "Pass histogram instance/name to `measure_yabeda_histogram`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ measures = filter_matching_changes(Yabeda::TestAdapter.instance.histograms.fetch(metric))
34
+
35
+ measures.values.any? { |measure| expected_value.nil? || values_match?(expected_value, measure) }
36
+ end
37
+
38
+ def match_when_negated(metric, block)
39
+ unless expected_value.nil?
40
+ raise NotImplementedError, <<~MSG
41
+ `expect {}.not_to measure_yabeda_histogram` doesn't support specifying values with `.with`
42
+ as it can lead to false positives.
43
+ MSG
44
+ end
45
+
46
+ block.call
47
+
48
+ measures = filter_matching_changes(Yabeda::TestAdapter.instance.histograms.fetch(metric))
49
+
50
+ measures.none?
51
+ end
52
+
53
+ def failure_message
54
+ "expected #{expected_formatted} " \
55
+ "to be changed #{"to #{expected} " unless expected_value.nil?}" \
56
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
57
+ "but #{actual_changes_message}"
58
+ end
59
+
60
+ def failure_message_when_negated
61
+ "expected #{expected_formatted} " \
62
+ "not to be incremented " \
63
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
64
+ "but #{actual_changes_message}"
65
+ end
66
+
67
+ def actual_changes_message
68
+ measures = Yabeda::TestAdapter.instance.histograms.fetch(metric)
69
+ if measures.empty?
70
+ "no changes of this gauge have been made"
71
+ elsif tags && measures.key?(tags)
72
+ "has been changed to #{measures.fetch(tags)} with tags #{::RSpec::Support::ObjectFormatter.format(tags)}"
73
+ else
74
+ "following changes have been made: #{::RSpec::Support::ObjectFormatter.format(measures)}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./base_matcher"
4
+
5
+ module Yabeda
6
+ module RSpec
7
+ # Checks whether Yabeda gauge was set to some value during test run or not
8
+ # @param metric [Yabeda::Gauge,String,Symbol] metric instance or name
9
+ # @return [Yabeda::RSpec::UpdateYabedaGauge]
10
+ def update_yabeda_gauge(metric)
11
+ UpdateYabedaGauge.new(metric)
12
+ end
13
+
14
+ # Custom matcher class with implementation for +update_yabeda_gauge+
15
+ class UpdateYabedaGauge < BaseMatcher
16
+ def with(value)
17
+ @expected_value = value
18
+ self
19
+ end
20
+
21
+ attr_reader :expected_value
22
+
23
+ def initialize(*)
24
+ super
25
+ return if metric.is_a? Yabeda::Gauge
26
+
27
+ raise ArgumentError, "Pass gauge instance/name to `update_yabeda_gauge`. Got #{metric.inspect} instead"
28
+ end
29
+
30
+ def match(metric, block)
31
+ block.call
32
+
33
+ updates = filter_matching_changes(Yabeda::TestAdapter.instance.gauges.fetch(metric))
34
+
35
+ updates.values.any? { |update| expected_value.nil? || values_match?(expected_value, update) }
36
+ end
37
+
38
+ def match_when_negated(metric, block)
39
+ unless expected_value.nil?
40
+ raise NotImplementedError, <<~MSG
41
+ `expect(Yabeda.metric_name).not_to update_yabeda_gauge` doesn't support specifying values with `.with`
42
+ as it can lead to false positives.
43
+ MSG
44
+ end
45
+
46
+ block.call
47
+
48
+ updates = filter_matching_changes(Yabeda::TestAdapter.instance.gauges.fetch(metric))
49
+
50
+ updates.none?
51
+ end
52
+
53
+ def failure_message
54
+ "expected #{expected_formatted} " \
55
+ "to be changed #{"to #{expected_value} " unless expected_value.nil?}" \
56
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
57
+ "but #{actual_changes_message}"
58
+ end
59
+
60
+ def failure_message_when_negated
61
+ "expected #{expected_formatted} " \
62
+ "not to be changed " \
63
+ "#{("with tags #{::RSpec::Support::ObjectFormatter.format(tags)} " if tags)}" \
64
+ "but #{actual_changes_message}"
65
+ end
66
+
67
+ def actual_changes_message
68
+ updates = Yabeda::TestAdapter.instance.gauges.fetch(metric)
69
+ if updates.empty?
70
+ "no changes of this gauge have been made"
71
+ elsif tags && updates.key?(tags)
72
+ "has been changed to #{updates.fetch(tags)} with tags #{::RSpec::Support::ObjectFormatter.format(tags)}"
73
+ else
74
+ "following changes have been made: #{::RSpec::Support::ObjectFormatter.format(updates)}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./testing"
4
+
5
+ module Yabeda
6
+ # RSpec integration for Yabeda: custom matchers, etc
7
+ module RSpec
8
+ end
9
+ end
10
+
11
+ require_relative "./rspec/increment_yabeda_counter"
12
+ require_relative "./rspec/update_yabeda_gauge"
13
+ require_relative "./rspec/measure_yabeda_histogram"
14
+
15
+ ::RSpec.configure do |config|
16
+ config.before(:suite) do
17
+ Yabeda.configure! unless Yabeda.already_configured?
18
+ end
19
+
20
+ config.after(:each) do
21
+ Yabeda::TestAdapter.instance.reset!
22
+ end
23
+
24
+ config.include(Yabeda::RSpec)
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ require_relative "./base_adapter"
6
+
7
+ module Yabeda
8
+ # Fake monitoring system adapter that collects latest metric values for later inspection
9
+ class TestAdapter < BaseAdapter
10
+ include Singleton
11
+
12
+ attr_reader :counters, :gauges, :histograms
13
+
14
+ def initialize
15
+ @counters = Hash.new { |ch, ck| ch[ck] = Hash.new { |th, tk| th[tk] = 0 } }
16
+ @gauges = Hash.new { |gh, gk| gh[gk] = Hash.new { |th, tk| th[tk] = nil } }
17
+ @histograms = Hash.new { |hh, hk| hh[hk] = Hash.new { |th, tk| th[tk] = nil } }
18
+ end
19
+
20
+ # Call this method after every test example to quickly get blank state for the next test example
21
+ def reset!
22
+ [@counters, @gauges, @histograms].each do |collection|
23
+ collection.each_value(&:clear) # Reset tag-values hash to be empty
24
+ end
25
+ end
26
+
27
+ def register_counter!(metric)
28
+ @counters[metric]
29
+ end
30
+
31
+ def register_gauge!(metric)
32
+ @gauges[metric]
33
+ end
34
+
35
+ def register_histogram!(metric)
36
+ @histograms[metric]
37
+ end
38
+
39
+ def perform_counter_increment!(counter, tags, increment)
40
+ @counters[counter][tags] += increment
41
+ end
42
+
43
+ def perform_gauge_set!(gauge, tags, value)
44
+ @gauges[gauge][tags] = value
45
+ end
46
+
47
+ def perform_histogram_measure!(histogram, tags, value)
48
+ @histograms[histogram][tags] = value
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Include this file to get things prepared for testing
4
+
5
+ require_relative "./test_adapter"
6
+
7
+ Yabeda.register_adapter(:test, Yabeda::TestAdapter.instance)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yabeda
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yabeda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Novikov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-30 00:00:00.000000000 Z
11
+ date: 2021-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
@@ -166,7 +166,14 @@ files:
166
166
  - lib/yabeda/histogram.rb
167
167
  - lib/yabeda/metric.rb
168
168
  - lib/yabeda/railtie.rb
169
+ - lib/yabeda/rspec.rb
170
+ - lib/yabeda/rspec/base_matcher.rb
171
+ - lib/yabeda/rspec/increment_yabeda_counter.rb
172
+ - lib/yabeda/rspec/measure_yabeda_histogram.rb
173
+ - lib/yabeda/rspec/update_yabeda_gauge.rb
169
174
  - lib/yabeda/tags.rb
175
+ - lib/yabeda/test_adapter.rb
176
+ - lib/yabeda/testing.rb
170
177
  - lib/yabeda/version.rb
171
178
  - yabeda-logo.png
172
179
  - yabeda.gemspec