# frozen_string_literal: true require_relative "abstract_unit" require "active_support/core_ext/module/delegation" module Notifications class TestCase < ActiveSupport::TestCase def setup @old_notifier = ActiveSupport::Notifications.notifier @notifier = ActiveSupport::Notifications::Fanout.new ActiveSupport::Notifications.notifier = @notifier @events = [] @named_events = [] @subscription = @notifier.subscribe { |*args| @events << event(*args) } @named_subscription = @notifier.subscribe("named.subscription") { |*args| @named_events << event(*args) } end def teardown ActiveSupport::Notifications.notifier = @old_notifier end private def event(*args) ActiveSupport::Notifications::Event.new(*args) end end class SubscribeEventObjectsTest < TestCase def test_subscribe_events events = [] @notifier.subscribe do |event| events << event end ActiveSupport::Notifications.instrument("foo") event = events.first assert event, "should have an event" assert_operator event.allocations, :>, 0 assert_operator event.cpu_time, :>, 0 assert_operator event.idle_time, :>=, 0 assert_operator event.duration, :>, 0 end def test_subscribe_to_events_where_payload_is_changed_during_instrumentation @notifier.subscribe do |event| assert_equal "success!", event.payload[:my_key] end ActiveSupport::Notifications.instrument("foo") do |payload| payload[:my_key] = "success!" end end def test_subscribe_to_events_can_handle_nested_hashes_in_the_paylaod @notifier.subscribe do |event| assert_equal "success!", event.payload[:some_key][:key_one] assert_equal "great_success!", event.payload[:some_key][:key_two] end ActiveSupport::Notifications.instrument("foo", some_key: { key_one: "success!" }) do |payload| payload[:some_key][:key_two] = "great_success!" end end def test_subscribe_via_top_level_api old_notifier = ActiveSupport::Notifications.notifier ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new event = nil ActiveSupport::Notifications.subscribe("foo") do |e| event = e end ActiveSupport::Notifications.instrument("foo") do 100.times { Object.new } # allocate at least 100 objects end assert event assert_operator event.allocations, :>=, 100 ensure ActiveSupport::Notifications.notifier = old_notifier end def test_subscribe_with_a_single_arity_lambda_listener event_name = nil listener = ->(event) do event_name = event.name end @notifier.subscribe(&listener) ActiveSupport::Notifications.instrument("event_name") assert_equal "event_name", event_name end def test_subscribe_with_a_single_arity_callable_listener event_name = nil listener = Class.new do define_method :call do |event| event_name = event.name end end @notifier.subscribe(nil, listener.new) ActiveSupport::Notifications.instrument("event_name") assert_equal "event_name", event_name end end class TimedAndMonotonicTimedSubscriberTest < TestCase def test_subscribe event_name = "foo" class_of_started = nil class_of_finished = nil ActiveSupport::Notifications.subscribe(event_name) do |name, started, finished, unique_id, data| class_of_started = started.class class_of_finished = finished.class end ActiveSupport::Notifications.instrument(event_name) assert_equal [Time, Time], [class_of_started, class_of_finished] end def test_monotonic_subscribe event_name = "foo" class_of_started = nil class_of_finished = nil ActiveSupport::Notifications.monotonic_subscribe(event_name) do |name, started, finished, unique_id, data| class_of_started = started.class class_of_finished = finished.class end ActiveSupport::Notifications.instrument(event_name) assert_equal [Float, Float], [class_of_started, class_of_finished] end end class BuildHandleTest < TestCase def test_interleaved_event event_name = "foo" actual_times = [] ActiveSupport::Notifications.subscribe(event_name) do |name, started, finished, unique_id, data| actual_times << [started, finished] end times = (1..4).map { |s| Time.new(2020, 1, 1) + s } instrumenter = ActiveSupport::Notifications.instrumenter travel_to times[0] handle1 = instrumenter.build_handle(event_name, {}) handle2 = instrumenter.build_handle(event_name, {}) handle1.start travel_to times[1] handle2.start travel_to times[2] handle1.finish travel_to times[3] handle2.finish assert_equal [ # from when start1 was returned, to when its state passed to finish [times[0], times[2]], # from when start2 was returned, to when its state passed to finish [times[1], times[3]], ], actual_times end def test_subscribed_interleaved_with_event instrumenter = ActiveSupport::Notifications.instrumenter name = "foo" events1 = [] events2 = [] callback1 = lambda { |event| events1 << event } callback2 = lambda { |event| events2 << event } ActiveSupport::Notifications.subscribed(callback1, name) do handle = instrumenter.build_handle(name, {}) handle.start ActiveSupport::Notifications.subscribed(callback2, name) do handle.finish end end assert_equal 1, events1.size assert_empty events2 assert_equal name, events1[0].name assert events1[0].time assert events1[0].end end end class SubscribedTest < TestCase def test_subscribed name = "foo" name2 = name * 2 expected = [name, name] events = [] callback = lambda { |*_| events << _.first } ActiveSupport::Notifications.subscribed(callback, name) do ActiveSupport::Notifications.instrument(name) ActiveSupport::Notifications.instrument(name2) ActiveSupport::Notifications.instrument(name) end assert_equal expected, events ActiveSupport::Notifications.instrument(name) assert_equal expected, events end def test_subscribed_all_messages name = "foo" name2 = name * 2 expected = [name, name2, name] events = [] callback = lambda { |*_| events << _.first } ActiveSupport::Notifications.subscribed(callback) do ActiveSupport::Notifications.instrument(name) ActiveSupport::Notifications.instrument(name2) ActiveSupport::Notifications.instrument(name) end assert_equal expected, events ActiveSupport::Notifications.instrument(name) assert_equal expected, events end def test_subscribing_to_instrumentation_while_inside_it # the repro requires that there are no evented subscribers for the "foo" event, # so we have to duplicate some of the setup code old_notifier = ActiveSupport::Notifications.notifier ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new ActiveSupport::Notifications.subscribe("foo", TestSubscriber.new) assert_nothing_raised do ActiveSupport::Notifications.instrument("foo") do ActiveSupport::Notifications.subscribe("foo") { } end end ensure ActiveSupport::Notifications.notifier = old_notifier end def test_timed_subscribed event_name = "foo" class_of_started = nil class_of_finished = nil callback = lambda do |name, started, finished, unique_id, data| class_of_started = started.class class_of_finished = finished.class end ActiveSupport::Notifications.subscribed(callback, event_name) do ActiveSupport::Notifications.instrument(event_name) end ActiveSupport::Notifications.instrument(event_name) assert_equal [Time, Time], [class_of_started, class_of_finished] end def test_monotonic_timed_subscribed event_name = "foo" class_of_started = nil class_of_finished = nil callback = lambda do |name, started, finished, unique_id, data| class_of_started = started.class class_of_finished = finished.class end ActiveSupport::Notifications.subscribed(callback, event_name, monotonic: true) do ActiveSupport::Notifications.instrument(event_name) end ActiveSupport::Notifications.instrument(event_name) assert_equal [Float, Float], [class_of_started, class_of_finished] end end class InspectTest < TestCase def test_inspect_output_is_small expected = "#" assert_equal expected, @notifier.inspect end end class UnsubscribeTest < TestCase def test_unsubscribing_removes_a_subscription @notifier.publish :foo @notifier.wait assert_equal [[:foo]], @events @notifier.unsubscribe(@subscription) @notifier.publish :foo @notifier.wait assert_equal [[:foo]], @events end def test_unsubscribing_by_name_removes_a_subscription @notifier.publish "named.subscription", :foo @notifier.wait assert_equal [["named.subscription", :foo]], @named_events @notifier.unsubscribe("named.subscription") @notifier.publish "named.subscription", :foo @notifier.wait assert_equal [["named.subscription", :foo]], @named_events end def test_unsubscribing_by_name_leaves_the_other_subscriptions @notifier.publish "named.subscription", :foo @notifier.wait assert_equal [["named.subscription", :foo]], @events @notifier.unsubscribe("named.subscription") @notifier.publish "named.subscription", :foo @notifier.wait assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events end def test_unsubscribing_by_name_leaves_regexp_matched_subscriptions @matched_events = [] @notifier.subscribe(/subscription/) { |*args| @matched_events << event(*args) } @notifier.publish("named.subscription", :before) @notifier.wait [@events, @named_events, @matched_events].each do |collector| assert_includes(collector, ["named.subscription", :before]) end @notifier.unsubscribe("named.subscription") @notifier.publish("named.subscription", :after) @notifier.publish("other.subscription", :after) @notifier.wait assert_includes(@events, ["named.subscription", :after]) assert_includes(@events, ["other.subscription", :after]) assert_includes(@matched_events, ["other.subscription", :after]) assert_not_includes(@matched_events, ["named.subscription", :after]) assert_not_includes(@named_events, ["named.subscription", :after]) end private def event(*args) args end end class TestSubscriber attr_reader :starts, :finishes, :publishes def initialize @starts = [] @finishes = [] @publishes = [] end def start(*args); @starts << args; end def finish(*args); @finishes << args; end def publish(*args); @publishes << args; end end class SyncPubSubTest < TestCase def test_events_are_published_to_a_listener @notifier.publish :foo @notifier.wait assert_equal [[:foo]], @events end def test_publishing_multiple_times_works @notifier.publish :foo @notifier.publish :foo @notifier.wait assert_equal [[:foo], [:foo]], @events end def test_publishing_after_a_new_subscribe_works @notifier.publish :foo @notifier.publish :foo @notifier.subscribe("not_existent") { |event| @events << event } @notifier.publish :foo @notifier.publish :foo @notifier.wait assert_equal [[:foo]] * 4, @events end def test_log_subscriber_with_string events = [] @notifier.subscribe("1") { |*args| events << args } @notifier.publish "1" @notifier.publish "1.a" @notifier.publish "a.1" @notifier.wait assert_equal [["1"]], events end def test_log_subscriber_with_pattern events = [] @notifier.subscribe(/\d/) { |*args| events << args } @notifier.publish "1" @notifier.publish "a.1" @notifier.publish "1.a" @notifier.wait assert_equal [["1"], ["a.1"], ["1.a"]], events end def test_multiple_log_subscribers @another = [] @notifier.subscribe { |*args| @another << args } @notifier.publish :foo @notifier.wait assert_equal [[:foo]], @events assert_equal [[:foo]], @another end def test_publish_with_subscriber subscriber = TestSubscriber.new @notifier.subscribe nil, subscriber @notifier.publish :foo assert_equal [[:foo]], subscriber.publishes end private def event(*args) args end end class InstrumentationTest < TestCase delegate :instrument, to: ActiveSupport::Notifications def test_instrument_returns_block_result assert_equal 2, instrument(:awesome) { 1 + 1 } end def test_instrument_yields_the_payload_for_further_modification assert_equal 2, instrument(:awesome) { |p| p[:result] = 1 + 1 } assert_equal 1, @events.size assert_equal :awesome, @events.first.name assert_equal Hash[result: 2], @events.first.payload end def test_instrumenter_exposes_its_id assert_equal 20, ActiveSupport::Notifications.instrumenter.id.size end def test_nested_events_can_be_instrumented instrument(:awesome, payload: "notifications") do instrument(:wot, payload: "child") do 1 + 1 end assert_equal 1, @events.size assert_equal :wot, @events.first.name assert_equal Hash[payload: "child"], @events.first.payload end assert_equal 2, @events.size assert_equal :awesome, @events.last.name assert_equal Hash[payload: "notifications"], @events.last.payload end def test_instrument_publishes_when_exception_is_raised begin instrument(:awesome, payload: "notifications") do raise "FAIL" end rescue RuntimeError => e assert_equal "FAIL", e.message end assert_equal 1, @events.size assert_equal Hash[payload: "notifications", exception: ["RuntimeError", "FAIL"], exception_object: e], @events.last.payload end def test_event_is_pushed_even_without_block instrument(:awesome, payload: "notifications") assert_equal 1, @events.size assert_equal :awesome, @events.last.name assert_equal Hash[payload: "notifications"], @events.last.payload end end class EventTest < TestCase def test_events_are_initialized_with_details time = Time.now.to_f event = event(:foo, time, time + 0.01, random_id, {}) assert_equal :foo, event.name assert_in_epsilon 10.0, event.duration, 0.01 end def test_event_cpu_time_does_not_raise_error_when_start_or_finished_not_called time = Time.now event = event(:foo, time, time + 0.01, random_id, {}) assert_equal 0, event.cpu_time end def test_events_consumes_information_given_as_payload event = event(:foo, Process.clock_gettime(Process::CLOCK_MONOTONIC), Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1, random_id, payload: :bar) assert_equal Hash[payload: :bar], event.payload end def test_subscribe_raises_error_on_non_supported_arguments notifier = ActiveSupport::Notifications::Fanout.new assert_raises ArgumentError do notifier.subscribe(:symbol) { |*_| } end assert_raises ArgumentError do notifier.subscribe(Object.new) { |*_| } end end private def random_id @random_id ||= SecureRandom.hex(10) end end end