From b247622020595d4a0e31ffc5bf73e12fc7f3f6a2 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Wed, 29 Nov 2023 13:37:58 +0200 Subject: [PATCH] add send_metrics matcher, add base_stats instrumentation tests --- lib/prometheus_exporter/ext/rspec.rb | 10 +++ lib/prometheus_exporter/ext/rspec/matchers.rb | 5 ++ .../ext/rspec/send_metrics_matcher.rb | 86 +++++++++++++++++++ .../ext/rspec/test_client.rb | 30 +++++++ .../ext/instrumentation/base_stats_spec.rb | 50 +++++++++++ spec/spec_helper.rb | 16 +++- 6 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb create mode 100644 lib/prometheus_exporter/ext/rspec/test_client.rb create mode 100644 spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb diff --git a/lib/prometheus_exporter/ext/rspec.rb b/lib/prometheus_exporter/ext/rspec.rb index 4e659d4..50397c7 100644 --- a/lib/prometheus_exporter/ext/rspec.rb +++ b/lib/prometheus_exporter/ext/rspec.rb @@ -1,7 +1,17 @@ # frozen_string_literal: true +require 'prometheus_exporter/client' require_relative 'rspec/matchers' +require_relative 'rspec/test_client' + +# Setups default client before it used anywhere. +# use `include_examples :observes_prometheus_metrics` in specs +PrometheusExporter::Client.default = PrometheusExporter::Ext::RSpec::TestClient.instance RSpec.configure do |config| config.include PrometheusExporter::Ext::RSpec::Matchers + + config.before do + PrometheusExporter::Ext::RSpec::TestClient.instance.reset + end end diff --git a/lib/prometheus_exporter/ext/rspec/matchers.rb b/lib/prometheus_exporter/ext/rspec/matchers.rb index 12a27b9..0e0adaa 100644 --- a/lib/prometheus_exporter/ext/rspec/matchers.rb +++ b/lib/prometheus_exporter/ext/rspec/matchers.rb @@ -2,9 +2,14 @@ require 'date' require_relative 'metric_matcher' +require_relative 'send_metrics_matcher' module PrometheusExporter::Ext::RSpec module Matchers + def send_metrics(expected = nil) + PrometheusExporter::Ext::RSpec::SendMetricsMatcher.new(expected) + end + def a_prometheus_metric(klass, name) MetricMatcher.new(klass, name) end diff --git a/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb new file mode 100644 index 0000000..e84348a --- /dev/null +++ b/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module PrometheusExporter::Ext::RSpec + class SendMetricsMatcher + include RSpec::Matchers::DSL::DefaultImplementations + include RSpec::Matchers + include RSpec::Matchers::Composable + + attr_reader :expected, :actual + + def initialize(expected) + @expected = expected&.map { |metric| deep_stringify_keys(metric) } + @ordered = false + @times = nil + end + + def name + 'sends metrics to prometheus' + end + + def supports_block_expectations? + true + end + + def matches?(actual_proc) + raise ArgumentError, "#{name} matcher supports only block expectations" unless actual_proc.is_a?(Proc) + + metrics_before = PrometheusExporter::Ext::RSpec::TestClient.instance.metrics + actual_proc.call + metrics_after = PrometheusExporter::Ext::RSpec::TestClient.instance.metrics - metrics_before + @actual = metrics_after.map { |metric| deep_stringify_keys(metric) } + + if expected + expected_value = @ordered ? expected : match_array(expected) + values_match?(expected_value, actual) + elsif @times + values_match?(@times, actual.size) + else + actual.size >= 1 + end + end + + def failure_message + if expected + expected_value = @ordered ? expected : match_array(expected) + +"expected #{name} to receive #{description_of(expected_value)}, but got\n #{description_of(actual)}" + elsif @times + values_match?(@times, actual.size) + +"expected #{name} to receive #{@times} metrics, but got #{actual.size}\n #{description_of(actual)}" + else + actual.size + +"expected #{name} to receive more than 1 metric, but got #{actual.size}\n #{description_of(actual)}" + end + end + + def ordered + raise ArgumentError, 'ordered cannot be when expected not provided' if expected.nil? + raise ArgumentError, 'ordered cannot be used with times' if @times + + @ordered = true + self + end + + def times(qty) + raise ArgumentError, 'times argument must be an integer' unless qty.is_a?(Integer) + raise ArgumentError, 'times argument must be >= 1' unless qty >= 1 + raise ArgumentError, 'ordered cannot be when expected is provided' unless expected.nil? + raise ArgumentError, 'ordered cannot be used with times' if @ordered + + @times = qty + self + end + + def description_of(object) + RSpec::Support::ObjectFormatter.new(nil).format(object) + end + + private + + def deep_stringify_keys(hash) + hash.transform_keys(&:to_s).transform_values do |value| + value.is_a?(Hash) ? deep_stringify_keys(value) : value + end + end + end +end diff --git a/lib/prometheus_exporter/ext/rspec/test_client.rb b/lib/prometheus_exporter/ext/rspec/test_client.rb new file mode 100644 index 0000000..334672b --- /dev/null +++ b/lib/prometheus_exporter/ext/rspec/test_client.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'singleton' + +module PrometheusExporter::Ext::RSpec + class TestClient + include Singleton + + def initialize + super + reset + end + + def metrics + @metrics.dup + end + + def send_json(data) + @metrics << data + end + + def reset + @metrics = [] + end + + def stop + nil + end + end +end diff --git a/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb b/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb new file mode 100644 index 0000000..9b4ab50 --- /dev/null +++ b/spec/prometheus_exporter/ext/instrumentation/base_stats_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe PrometheusExporter::Ext::Instrumentation::BaseStats do + subject do + instrumentation.collect(data_list) + end + + let(:instrumentation) { TestInstrumentation.new } + let(:data_list) do + [ + { foo: 123, bar: 456, labels: { qwe: 'asd' } }, + { foo: 124, bar: 457, labels: { qwe: 'zxc' } } + ] + end + + it 'sends metrics' do + expect { subject }.to send_metrics + end + + it 'sends exactly 2 metrics' do + expect { subject }.to send_metrics.times(2) + end + + it 'sends correct metrics' do + expect { subject }.to send_metrics( + [ + { + type: 'test', + foo: 123, bar: 456, + labels: { qwe: 'asd' }, + metric_labels: {} + }, + { + type: 'test', + foo: 124, bar: 457, + labels: { qwe: 'zxc' }, + metric_labels: {} + } + ] + ) + end + + context 'with empty data list' do + let(:data_list) { [] } + + it 'sends correct metrics' do + expect { subject }.not_to send_metrics + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 39b1c55..27da4b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,19 @@ # frozen_string_literal: true +require 'prometheus_exporter' require 'prometheus_exporter/ext' -require 'prometheus_exporter/ext/rspec/matchers' +require 'prometheus_exporter/ext/rspec' +require 'prometheus_exporter/ext/instrumentation/base_stats' + +class TestInstrumentation < PrometheusExporter::Ext::Instrumentation::BaseStats + self.type = 'test' + + def collect(data_list) + data_list.each do |data| + collect_data(data) + end + end +end RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -13,6 +25,4 @@ config.expect_with :rspec do |c| c.syntax = :expect end - - config.include PrometheusExporter::Ext::RSpec::Matchers end