From 7d9b4e950b79bc284b527d9193ea18f311b02f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Busqu=C3=A9?= Date: Tue, 27 Jul 2021 12:48:35 +0200 Subject: [PATCH] Introduce Spree::Event's test interface to run only selected listeners Given the global nature of our event bus, we need a system to scope the execution of a block to a selected list of subscribers. That's useful for testing purposes, as we need to be sure that others subscribers are not interfering with our expectations. This commit introduces a `Spree::Event.performing_only(listeners)` method. It takes a block during the execution of which only the provided listeners are subscribed: ```ruby listener1 = Spree::Event.subscribe('foo') { do_something } listener2 = Spree::Event.subscribe('foo') { do_something_else } Spree::Event.performing_only(listener1) do Spree::Event.fire('foo') # only listener1 will run end Spree::Event.fire('foo') # both listener1 & listener2 will run ``` This behavior is only available for the new `Spree::Event::Adapters::Default` adapter. We only need that for testing purposes, so the method is made available after calling `Spree::Event.enable_test_interface`. It prevents the main `Spree::Event` API from being bloated and sends a more explicit message to users. We also add a `Spree::Subscriber#listeners` method, which returns the set of generated listeners for a given subscriber module. It's called automatically by `Spree::Event.performing_only` so that users can directly specify that they only want the listeners for a given subscriber module to be run. `Spree::Subscriber#listeners` accepts an array of event names as arguments in case more fine-grained control is required. ```ruby module EmailSubscriber include Spree::Event::Subscriber event_action :foo event_action :bar def foo(_event) do_something end def bar(_event) do_something_else end end Spree::Event.performing_only(EmailSubscriber) do Spree::Event.fire('foo') # both foo & bar methods will run end Spree::Event.performing_only(EmailSubscriber.listeners('foo')) do Spree::Event.fire('foo') # only foo method will run end ``` A specialized `Spree::Event.performing_nothing` method calls `Spree::Event.performing_only` with no listeners at all. --- core/lib/spree/event/adapters/default.rb | 9 +- .../event/adapters/deprecation_handler.rb | 4 +- core/lib/spree/event/listener.rb | 5 + core/lib/spree/event/subscriber.rb | 41 ++++ core/lib/spree/event/subscriber_registry.rb | 16 ++ core/lib/spree/event/test_interface.rb | 93 +++++++++ .../lib/spree/event/adapters/default_spec.rb | 19 ++ core/spec/lib/spree/event/listener_spec.rb | 8 + .../spree/event/subscriber_registry_spec.rb | 43 ++++ core/spec/lib/spree/event/subscriber_spec.rb | 34 ++++ .../lib/spree/event/test_interface_spec.rb | 184 ++++++++++++++++++ 11 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 core/lib/spree/event/test_interface.rb create mode 100644 core/spec/lib/spree/event/test_interface_spec.rb diff --git a/core/lib/spree/event/adapters/default.rb b/core/lib/spree/event/adapters/default.rb index 16e1d1f04e..dec319219a 100644 --- a/core/lib/spree/event/adapters/default.rb +++ b/core/lib/spree/event/adapters/default.rb @@ -28,8 +28,8 @@ class Default # @api private attr_reader :listeners - def initialize - @listeners = [] + def initialize(listeners = []) + @listeners = listeners end # @api private @@ -57,6 +57,11 @@ def unsubscribe(subscriber_or_event_name) end end + # @api private + def with_listeners(listeners) + self.class.new(listeners) + end + private def listeners_for_event(event_name) diff --git a/core/lib/spree/event/adapters/deprecation_handler.rb b/core/lib/spree/event/adapters/deprecation_handler.rb index 28f35433db..5bfb28787a 100644 --- a/core/lib/spree/event/adapters/deprecation_handler.rb +++ b/core/lib/spree/event/adapters/deprecation_handler.rb @@ -12,7 +12,7 @@ module DeprecationHandler CI_LEGACY_ADAPTER_ENV_KEY = 'CI_LEGACY_EVENT_BUS_ADAPTER' - def self.legacy_adapter?(adapter) + def self.legacy_adapter?(adapter = Spree::Config.events.adapter) adapter == LEGACY_ADAPTER end @@ -20,7 +20,7 @@ def self.legacy_adapter_set_by_env return LEGACY_ADAPTER if ENV[CI_LEGACY_ADAPTER_ENV_KEY].present? end - def self.render_deprecation_message?(adapter) + def self.render_deprecation_message?(adapter = Spree::Config.events.adapter) legacy_adapter?(adapter) && legacy_adapter_set_by_env.nil? end end diff --git a/core/lib/spree/event/listener.rb b/core/lib/spree/event/listener.rb index 4cfb986824..f40dca87fb 100644 --- a/core/lib/spree/event/listener.rb +++ b/core/lib/spree/event/listener.rb @@ -49,6 +49,11 @@ def unsubscribe(event_name) @exclusions << event_name end + # @api private + def listeners + [self] + end + private def excludes?(event_name) diff --git a/core/lib/spree/event/subscriber.rb b/core/lib/spree/event/subscriber.rb index 78773ae393..4df62b6d3e 100644 --- a/core/lib/spree/event/subscriber.rb +++ b/core/lib/spree/event/subscriber.rb @@ -81,6 +81,47 @@ def activate def deactivate(event_action_name = nil) Spree::Event.subscriber_registry.deactivate_subscriber(self, event_action_name) end + + # Returns the generated listeners for the subscriber + # + # This method is only available when using + # [Spree::Event::Adapters::Default] adapter + # + # When a {Subscriber} is registered, the corresponding listeners are + # automatically generated, i.e., the returning values for + # {Spree::Event.subscribe} that encapsulate the logic to be performed. + # + # The listeners to obtain can be restricted to only certain events by providing + # their names: + # + # @example + # + # module EmailSender + # include Spree::Event::Subscriber + # + # event_action :order_finalized + # event_action :confirm_reimbursement + # + # def order_finalized(event) + # # ... + # end + # + # def confirm_reimbursement(event) + # # ... + # end + # end + # + # EmailSender.activate + # EmailSender.listeners.count # => 2 + # EmailSender.listeners("order_finalized").count # => 1 + # + # @param event_names [Array] + # @return [Array] + # @raise [RuntimeError] When adapter is + # [Spree::Event::Adapters::ActiveSupportNotifications] + def listeners(*event_names) + Spree::Event.subscriber_registry.listeners(self, event_names: event_names) + end end end end diff --git a/core/lib/spree/event/subscriber_registry.rb b/core/lib/spree/event/subscriber_registry.rb index 8f17531bd3..0b56722feb 100644 --- a/core/lib/spree/event/subscriber_registry.rb +++ b/core/lib/spree/event/subscriber_registry.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'spree/event/adapters/deprecation_handler' + module Spree module Event class SubscriberRegistry @@ -50,6 +52,20 @@ def deactivate_subscriber(subscriber, event_action_name = nil) end end + def listeners(subscriber, event_names: []) + raise <<~MSG if Adapters::DeprecationHandler.legacy_adapter? + This method is only available with the new adapter Spree::Event::Adapters::Default + MSG + + registry[subscriber.name].values.yield_self do |listeners| + if event_names.empty? + listeners + else + listeners.select { |listener| event_names.map(&:to_s).include?(listener.pattern) } + end + end + end + private attr_reader :registry diff --git a/core/lib/spree/event/test_interface.rb b/core/lib/spree/event/test_interface.rb new file mode 100644 index 0000000000..ca4d3768a3 --- /dev/null +++ b/core/lib/spree/event/test_interface.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spree/event' + +module Spree + module Event + # Test helpers for {Spree::Event} + # + # This module defines test helpers methods for {Spree::Event}. They can be + # made available to {Spree::Event} when {Spree::Event.enable_test_interface} + # is called. + # + # If you prefer, you can directly call them from + # `Spree::Event::TestInterface}. + module TestInterface + # @see {Spree::Event::TestInterface} + module Methods + # Perform only given listeners for the duration of the block + # + # Temporarily deactivate all subscribed listeners and listen only to the + # provided ones for the duration of the block. + # + # @example + # Spree::Event.enable_test_interface + # + # listener1 = Spree::Event.subscribe('foo') { do_something } + # listener2 = Spree::Event.subscribe('foo') { do_something_else } + # + # Spree::Event.performing_only(listener1) do + # Spree::Event.fire('foo') # This will run only `listener1` + # end + # + # Spree::Event.fire('foo') # This will run both `listener1` & `listener2` + # + # {Spree::Event::Subscriber} modules can also be given to unsubscribe from + # all listeners generated from it: + # + # @example + # Spree::Event.performing_only(EmailSubscriber) {} + # + # You can gain more fine-grained control thanks to + # {Spree::Event::Subscribe#listeners}: + # + # @example + # Spree::Event.performing_only(EmailSubscriber.listeners('order_finalized')) {} + # + # You can mix different ways of specifying listeners without problems: + # + # @example + # Spree::Event.performing_only(EmailSubscriber, listener1) {} + # + # @param listeners_and_subscribers [Spree::Event::Listener, + # Array, Spree::Event::Subscriber] + # @yield While the block executes only provided listeners will run + def performing_only(*listeners_and_subscribers) + adapter_in_use = Spree::Event.default_adapter + listeners = listeners_and_subscribers.flatten.map(&:listeners) + Spree::Config.events.adapter = adapter_in_use.with_listeners(listeners.flatten) + yield + ensure + Spree::Config.events.adapter = adapter_in_use + end + + # Perform no listeners for the duration of the block + # + # It's a specialized version of {#performing_only} that provides no + # listeners. + # + # @yield While the block executes no listeners will run + # + # @see Spree::Event::TestInterface#performing_only + def performing_nothing(&block) + performing_only(&block) + end + end + + extend Methods + end + + # Adds test methods to {Spree::Event} + # + # @raise [RuntimeError] when {Spree::Event::Configuration#adapter} is set to + # the legacy adapter {Spree::Event::Adapters::ActiveSupportNotifications}. + def enable_test_interface + raise <<~MSG if deprecation_handler.legacy_adapter?(default_adapter) + Spree::Event's test interface is not supported when using the deprecated + adapter 'Spree::Event::Adapters::ActiveSupportNotifications'. + MSG + + extend(TestInterface::Methods) + end + end +end diff --git a/core/spec/lib/spree/event/adapters/default_spec.rb b/core/spec/lib/spree/event/adapters/default_spec.rb index bbc766f446..08f4732c48 100644 --- a/core/spec/lib/spree/event/adapters/default_spec.rb +++ b/core/spec/lib/spree/event/adapters/default_spec.rb @@ -189,6 +189,25 @@ def inc expect(dummy2.count).to be(1) end end + + describe '#with_listeners' do + it 'returns a new instance with given listeners', :aggregate_failures do + bus = described_class.new + dummy1, dummy2, dummy3 = Array.new(3) { counter.new } + listener1 = bus.subscribe('foo') { dummy1.inc } + listener2 = bus.subscribe('foo') { dummy2.inc } + listener3 = bus.subscribe('foo') { dummy3.inc } + + new_bus = bus.with_listeners([listener1, listener2]) + new_bus.fire('foo') + + expect(new_bus).not_to eq(bus) + expect(new_bus.listeners).to match_array([listener1, listener2]) + expect(dummy1.count).to be(1) + expect(dummy2.count).to be(1) + expect(dummy3.count).to be(0) + end + end end end end diff --git a/core/spec/lib/spree/event/listener_spec.rb b/core/spec/lib/spree/event/listener_spec.rb index 3f310c1b64..6f4ec76df1 100644 --- a/core/spec/lib/spree/event/listener_spec.rb +++ b/core/spec/lib/spree/event/listener_spec.rb @@ -88,4 +88,12 @@ end end end + + describe '#listeners' do + it 'returns a list containing only itself' do + listener = described_class.new(pattern: 'foo', block: -> {}) + + expect(listener.listeners).to eq([listener]) + end + end end diff --git a/core/spec/lib/spree/event/subscriber_registry_spec.rb b/core/spec/lib/spree/event/subscriber_registry_spec.rb index 33fd0ed88e..5cca82fd7e 100644 --- a/core/spec/lib/spree/event/subscriber_registry_spec.rb +++ b/core/spec/lib/spree/event/subscriber_registry_spec.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true +require 'rails_helper' require 'active_support/all' require 'spec_helper' require 'spree/event' +require 'spree/event/adapters/deprecation_handler' +require 'spree/event/listener' RSpec.describe Spree::Event::SubscriberRegistry do module N @@ -112,4 +115,44 @@ def other_event(event) end end end + + describe '#listeners' do + if Spree::Event::Adapters::DeprecationHandler.legacy_adapter? + it 'raises error' do + expect { subject.listeners(N) }.to raise_error /only available with the new adapter/ + end + else + before do + subject.register(N) + subject.activate_subscriber(N) + end + after { subject.deactivate_subscriber(N) } + + it 'returns all listeners that the subscriber generates', :aggregate_failures do + listeners = subject.listeners(N) + + expect(listeners.count).to be(2) + expect(listeners).to all be_a(Spree::Event::Listener) + end + + it 'can restrict by event names', :aggregate_failures do + listeners = subject.listeners(N, event_names: ['event_name']) + + expect(listeners.count).to be(1) + expect(listeners.first.pattern).to eq('event_name') + + listeners = subject.listeners(N, event_names: ['event_name', 'other_event']) + + expect(listeners.count).to be(2) + expect(listeners.map(&:pattern)).to match(['event_name', 'other_event']) + end + + it 'can event names as symbols', :aggregate_failures do + listeners = subject.listeners(N, event_names: [:event_name]) + + expect(listeners.count).to be(1) + expect(listeners.first.pattern).to eq('event_name') + end + end + end end diff --git a/core/spec/lib/spree/event/subscriber_spec.rb b/core/spec/lib/spree/event/subscriber_spec.rb index 06b006815b..c98ce5db67 100644 --- a/core/spec/lib/spree/event/subscriber_spec.rb +++ b/core/spec/lib/spree/event/subscriber_spec.rb @@ -1,19 +1,27 @@ # frozen_string_literal: true +require 'rails_helper' require 'active_support/all' require 'spec_helper' require 'spree/event' +require 'spree/event/adapters/deprecation_handler' +require 'spree/event/listener' RSpec.describe Spree::Event::Subscriber do module M include Spree::Event::Subscriber event_action :event_name + event_action :for_event_foo, event_name: :foo def event_name(event) # code that handles the event end + def for_event_foo(event) + # code that handles the event + end + def other_event(event) # not registered via event_action end @@ -83,4 +91,30 @@ def other_event(event) end end end + + unless Spree::Event::Adapters::DeprecationHandler.legacy_adapter? + describe '::listeners' do + before { M.activate } + after { M.deactivate } + + it 'returns all listeners that the subscriber generates when no arguments are given', :aggregate_failures do + listeners = M.listeners + + expect(listeners.count).to be(2) + expect(listeners.first).to be_a(Spree::Event::Listener) + end + + it 'can restrict by event names given as arguments', :aggregate_failures do + listeners = M.listeners('event_name') + + expect(listeners.count).to be(1) + expect(listeners.first.pattern).to eq('event_name') + + listeners = M.listeners('event_name', 'foo') + + expect(listeners.count).to be(2) + expect(listeners.map(&:pattern)).to match(['event_name', 'foo']) + end + end + end end diff --git a/core/spec/lib/spree/event/test_interface_spec.rb b/core/spec/lib/spree/event/test_interface_spec.rb new file mode 100644 index 0000000000..9e31477491 --- /dev/null +++ b/core/spec/lib/spree/event/test_interface_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'spree/event/adapters/deprecation_handler' +require 'spree/event/test_interface' + +RSpec.describe Spree::Event::TestInterface do + let(:counter) do + Class.new do + attr_reader :count + + def initialize + @count = 0 + end + + def inc + @count += 1 + end + end + end + + describe '#enable_test_interface' do + context 'when using the legacy adapter' do + it 'raises an error' do + adapter = Spree::Config.events.adapter + Spree::Config.events.adapter = Spree::Event::Adapters::ActiveSupportNotifications + + expect { + Spree::Event.enable_test_interface + }.to raise_error(/test interface is not supported/) + ensure + Spree::Config.events.adapter = adapter + end + end + end + + unless Spree::Event::Adapters::DeprecationHandler.legacy_adapter? + it 'can be accessed directly from TestInterface' do + dummy = counter.new + listener = Spree::Event.subscribe('foo') { dummy.inc } + + described_class.performing_only(listener) do + Spree::Event.fire('foo') + end + + expect(dummy.count).to be(1) + end + + describe '#performing_only' do + before { Spree::Event.enable_test_interface } + + it 'only performs given listeners for the duration of the block', :aggregate_failures do + dummy1, dummy2, dummy3 = Array.new(3) { counter.new } + listener1 = Spree::Event.subscribe('foo') { dummy1.inc } + listener2 = Spree::Event.subscribe('foo') { dummy2.inc } + listener3 = Spree::Event.subscribe('foo') { dummy3.inc } + + Spree::Event.performing_only(listener1, listener2) do + Spree::Event.fire('foo') + end + + expect(dummy1.count).to be(1) + expect(dummy2.count).to be(1) + expect(dummy3.count).to be(0) + end + + it 'performs again all the listeners once the block is done', :aggregate_failures do + dummy1, dummy2 = Array.new(2) { counter.new } + listener1 = Spree::Event.subscribe('foo') { dummy1.inc } + listener2 = Spree::Event.subscribe('foo') { dummy2.inc } + + Spree::Event.performing_only(listener1) do + Spree::Event.fire('foo') + end + + expect(dummy1.count).to be(1) + expect(dummy2.count).to be(0) + + Spree::Event.fire('foo') + + expect(dummy2.count).to be(1) + end + + it 'can extract listeners from a subscriber module', :aggregate_failures do + dummy1, dummy2 = Array.new(2) { counter.new } + Subscriber1 = Module.new do + include Spree::Event::Subscriber + + event_action :foo + + def foo(event) + event.payload[:dummy1].inc + end + end + Subscriber2 = Module.new do + include Spree::Event::Subscriber + + event_action :foo + + def foo(event) + event.payload[:dummy2].inc + end + end + Spree::Event.subscriber_registry.register(Subscriber1) + Spree::Event.subscriber_registry.register(Subscriber2) + [Subscriber1, Subscriber2].map(&:activate) + + Spree::Event.performing_only(Subscriber1) do + Spree::Event.fire('foo', dummy1: dummy1, dummy2: dummy2) + end + + expect(dummy1.count).to be(1) + expect(dummy2.count).to be(0) + ensure + Spree::Event.subscriber_registry.deactivate_subscriber(Subscriber1) + Spree::Event.subscriber_registry.deactivate_subscriber(Subscriber2) + end + + it 'can mix listeners and array of listeners', :aggregate_failures do + dummy1, dummy2 = Array.new(2) { counter.new } + listener = Spree::Event.subscribe('foo') { dummy1.inc } + Subscriber = Module.new do + include Spree::Event::Subscriber + + event_action :foo + + def foo(event) + event.payload[:dummy2].inc + end + end + Spree::Event.subscriber_registry.register(Subscriber) + Subscriber.activate + + Spree::Event.performing_only(listener, Subscriber) do + Spree::Event.fire('foo', dummy2: dummy2) + end + + expect(dummy1.count).to be(1) + expect(dummy2.count).to be(1) + ensure + Spree::Event.subscriber_registry.deactivate_subscriber(Subscriber) + end + + it 'can perform no listener at all' do + dummy = counter.new + listener = Spree::Event.subscribe('foo') { dummy.inc } + + Spree::Event.performing_only do + Spree::Event.fire('foo') + end + + expect(dummy.count).to be(0) + end + + it 'can override through an inner call' do + dummy = counter.new + listener = Spree::Event.subscribe('foo') { dummy.inc } + + Spree::Event.performing_only do + Spree::Event.performing_only(listener) do + Spree::Event.fire('foo') + end + end + + expect(dummy.count).to be(1) + end + end + + describe '#performing_nothing' do + before { Spree::Event.enable_test_interface } + + it 'performs no listener for the duration of the block' do + dummy = counter.new + listener = Spree::Event.subscribe('foo') { dummy.inc } + + Spree::Event.performing_nothing do + Spree::Event.fire('foo') + end + + expect(dummy.count).to be(0) + end + end + end +end