Skip to content

Commit

Permalink
Introduce Spree::Event's test interface to run only selected listeners
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
waiting-for-dev committed Dec 13, 2021
1 parent 9dbcad1 commit 7d9b4e9
Show file tree
Hide file tree
Showing 11 changed files with 452 additions and 4 deletions.
9 changes: 7 additions & 2 deletions core/lib/spree/event/adapters/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class Default
# @api private
attr_reader :listeners

def initialize
@listeners = []
def initialize(listeners = [])
@listeners = listeners
end

# @api private
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions core/lib/spree/event/adapters/deprecation_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ 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

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
Expand Down
5 changes: 5 additions & 0 deletions core/lib/spree/event/listener.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ def unsubscribe(event_name)
@exclusions << event_name
end

# @api private
def listeners
[self]
end

private

def excludes?(event_name)
Expand Down
41 changes: 41 additions & 0 deletions core/lib/spree/event/subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Symbol>]
# @return [Array<Spree::Event::Listener>]
# @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
16 changes: 16 additions & 0 deletions core/lib/spree/event/subscriber_registry.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'spree/event/adapters/deprecation_handler'

module Spree
module Event
class SubscriberRegistry
Expand Down Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions core/lib/spree/event/test_interface.rb
Original file line number Diff line number Diff line change
@@ -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::Listener>, 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
19 changes: 19 additions & 0 deletions core/spec/lib/spree/event/adapters/default_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions core/spec/lib/spree/event/listener_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 43 additions & 0 deletions core/spec/lib/spree/event/subscriber_registry_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions core/spec/lib/spree/event/subscriber_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading

0 comments on commit 7d9b4e9

Please sign in to comment.