yabeda 0.10.1 → 0.11.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 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