# frozen_string_literal: true # This module defines several assertion methods that can be used to verify that # your application is emitting the right StatsD metrics. # # Every metric type has its own assertion method, like {#assert_statsd_increment} # to assert `StatsD.increment` calls. You can also assert other properties of the # metric that was emitted, like the sample rate or presence of tags. # To check for the absence of metrics, use {#assert_no_statsd_calls}. # # @example Check for metric properties: # assert_statsd_measure('foo', sample_rate: 0.1, tags: ["bar"]) do # StatsD.measure('foo', sample_rate: 0.5, tags: ['bar','baz']) do # some_code_to_measure # end # end # # @example Check for multiple occurrences: # assert_statsd_increment('foo', times: 2) do # StatsD.increment('foo') # StatsD.increment('foo') # end # # @example Absence of metrics # assert_no_statsd_calls do # foo # end # # @example Handling exceptions # assert_statsd_increment('foo.error') do # # If we expect exceptions to occur, we have to handle them inside # # the block we pass to assert_statsd_increment. # assert_raises(RuntimeError) do # begin # attempt_foo # rescue # StatsD.increment('foo.error') # raise 'foo failed' # end # end # end module StatsD::Instrument::Assertions include StatsD::Instrument::Helpers # Asserts no metric occurred during the execution of the provided block. # # @param [Array] metric_names (default: []) The metric names that are not # allowed to happen inside the block. If this is set to `[]`, the assertion # will fail if any metric occurs. # @yield A block in which the specified metric should not occur. This block # should not raise any exceptions. # @return [void] # @raise [Minitest::Assertion] If an exception occurs, or if any metric (with the # provided names, or any), occurred during the execution of the provided block. def assert_no_statsd_calls(*metric_names, datagrams: nil, client: nil, &block) if datagrams.nil? raise LocalJumpError, "assert_no_statsd_calls requires a block" unless block_given? datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block) end datagrams.select! { |metric| metric_names.include?(metric.name) } unless metric_names.empty? assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(', ')} expected.") end # Asserts that a given counter metric occurred inside the provided block. # # @param [String] metric_name The name of the metric that should occur. # @param [Hash] options (see StatsD::Instrument::MetricExpectation.new) # @yield A block in which the specified metric should occur. This block # should not raise any exceptions. # @return [void] # @raise [Minitest::Assertion] If an exception occurs, or if the metric did # not occur as specified during the execution the block. def assert_statsd_increment(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.increment(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given timing metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_measure(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.measure(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given gauge metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_gauge(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.gauge(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given histogram metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_histogram(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.histogram(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given distribution metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_distribution(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.distribution(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given set metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_set(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.set(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that a given key/value metric occurred inside the provided block. # # @param metric_name (see #assert_statsd_increment) # @param options (see #assert_statsd_increment) # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_key_value(metric_name, datagrams: nil, client: nil, **options, &block) expectation = StatsD::Instrument::Expectation.key_value(metric_name, **options) assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block) end # Asserts that the set of provided metric expectations came true. # # Generally, it's recommended to use more specific assertion methods, like # {#assert_statsd_increment} and others. # # @private # @param [Array] expectations The set of # expectations to verify. # @yield (see #assert_statsd_increment) # @return [void] # @raise (see #assert_statsd_increment) def assert_statsd_expectations(expectations, datagrams: nil, client: nil, &block) if datagrams.nil? raise LocalJumpError, "assert_statsd_expectations requires a block" unless block_given? datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block) end expectations = Array(expectations) matched_expectations = [] expectations.each do |expectation| expectation_times = expectation.times expectation_times_remaining = expectation.times filtered_datagrams = datagrams.select { |m| m.type == expectation.type && m.name == expectation.name } if filtered_datagrams.empty? flunk("No StatsD calls for metric #{expectation.name} of type #{expectation.type} were made.") end filtered_datagrams.each do |datagram| next unless expectation.matches(datagram) if expectation_times_remaining == 0 flunk("Unexpected StatsD call; number of times this metric " \ "was expected exceeded: #{expectation.inspect}") end expectation_times_remaining -= 1 datagrams.delete(datagram) if expectation_times_remaining == 0 matched_expectations << expectation end end next if expectation_times_remaining == 0 msg = +"Metric expected #{expectation_times} times but seen " \ "#{expectation_times - expectation_times_remaining} " \ "times: #{expectation.inspect}." msg << "\nCaptured metrics with the same key: #{filtered_datagrams}" if filtered_datagrams.any? flunk(msg) end expectations -= matched_expectations unless expectations.empty? flunk("Unexpected StatsD calls; the following metric expectations " \ "were not satisfied: #{expectations.inspect}") end pass end # For backwards compatibility alias_method :assert_statsd_calls, :assert_statsd_expectations alias_method :assert_statsd_expectation, :assert_statsd_expectations private def capture_statsd_datagrams_with_exception_handling(client:, &block) capture_statsd_datagrams(client: client, &block) rescue => exception flunk(<<~MESSAGE) An exception occurred in the block provided to the StatsD assertion. #{exception.class.name}: #{exception.message} \t#{exception.backtrace.join("\n\t")} If this exception is expected, make sure to handle it using `assert_raises` inside the block provided to the StatsD assertion. MESSAGE end end