diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7c5d50..59ad9a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,11 @@ All notable changes to this project will be documented in this file, in reverse
- `LazyListenerAggregate`, which, provided a list of `LazyEventListeners` and/or
definitions to use to create them, acts as an aggregate for attaching a number
of such listeners at once.
+- [#20](https://github.com/zendframework/zend-eventmanager/pull/20) updates the
+ trait `Zend\EventManager\Test\EventListenerIntrospectionTrait` so that the
+ implementation will work with the v3 changes; the tests written for v2
+ continue to pass, allowing this trait to be used to provide compatibility
+ testing between v2 and v3.
### Deprecated
@@ -82,6 +87,34 @@ All notable changes to this project will be documented in this file, in reverse
- `FilterIterator::insert()` has been modified to raise an exception if the value provided is not a callable.
+## 2.6.2 - 2016-01-12
+
+### Added
+
+- [#19](https://github.com/zendframework/zend-eventmanager/pull/19) adds a new
+ trait, `Zend\EventManager\Test\EventListenerIntrospectionTrait`, intended for
+ composition in unit tests. It provides a number of methods that can be used
+ to retrieve listeners with or without associated priority, and the assertion
+ `assertListenerAtPriority(callable $listener, $priority, $event, EventManager $events, $message = '')`,
+ which can be used for testing that a listener was registered at the specified
+ priority with the specified event.
+
+ The features in this patch are intended to facilitate testing against both
+ version 2 and version 3 of zend-eventmanager, as it provides a consistent API
+ for retrieving lists of events and listeners between the two versions.
+
+### Deprecated
+
+- Nothing.
+
+### Removed
+
+- Nothing.
+
+### Fixed
+
+- Nothing.
+
## 2.6.0 - 2015-09-29
### Added
diff --git a/mkdocs.yml b/mkdocs.yml
index 830f928..51692ac 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,4 +15,4 @@ pages:
site_name: zend-eventmanager
site_description: 'zend-eventmanager: Powerful event bus system for Zend Framework'
repo_url: 'https://github.com/zendframework/zend-eventmanager'
-copyright: 'Copyright (c) 2015 Zend Technologies USA Inc.'
+copyright: 'Copyright (c) 2016 Zend Technologies USA Inc.'
diff --git a/src/Test/EventListenerIntrospectionTrait.php b/src/Test/EventListenerIntrospectionTrait.php
new file mode 100644
index 0000000..19d58df
--- /dev/null
+++ b/src/Test/EventListenerIntrospectionTrait.php
@@ -0,0 +1,146 @@
+setAccessible(true);
+ $listeners = $r->getValue($events);
+ return array_keys($listeners);
+ }
+
+ /**
+ * Retrieve an interable list of listeners for an event.
+ *
+ * Given an event and an event manager, returns an iterator with the
+ * listeners for that event, in priority order.
+ *
+ * If $withPriority is true, the key values will be the priority at which
+ * the given listener is attached.
+ *
+ * Do not pass $withPriority if you want to cast the iterator to an array,
+ * as many listeners will likely have the same priority, and thus casting
+ * will collapse to the last added.
+ *
+ * @param string $event
+ * @param EventManager $events
+ * @param bool $withPriority
+ * @return \Traversable
+ */
+ private function getListenersForEvent($event, EventManager $events, $withPriority = false)
+ {
+ $r = new ReflectionProperty($events, 'events');
+ $r->setAccessible(true);
+ $listeners = $r->getValue($events);
+
+ if (! isset($listeners[$event])) {
+ return $this->traverseListeners([]);
+ }
+
+ return $this->traverseListeners($listeners[$event], $withPriority);
+ }
+
+ /**
+ * Assert that a given listener exists at the specified priority.
+ *
+ * @param callable $expectedListener
+ * @param int $expectedPriority
+ * @param string $event
+ * @param EventManager $events
+ * @param string $message Failure message to use, if any.
+ */
+ private function assertListenerAtPriority(
+ callable $expectedListener,
+ $expectedPriority,
+ $event,
+ EventManager $events,
+ $message = ''
+ ) {
+ $message = $message ?: sprintf(
+ 'Listener not found for event "%s" and priority %d',
+ $event,
+ $expectedPriority
+ );
+ $listeners = $this->getListenersForEvent($event, $events, true);
+ $found = false;
+ foreach ($listeners as $priority => $listener) {
+ if ($listener === $expectedListener
+ && $priority === $expectedPriority
+ ) {
+ $found = true;
+ break;
+ }
+ }
+ Assert::assertTrue($found, $message);
+ }
+
+ /**
+ * Returns an indexed array of listeners for an event.
+ *
+ * Returns an indexed array of listeners for an event, in priority order.
+ * Priority values will not be included; use this only for testing if
+ * specific listeners are present, or for a count of listeners.
+ *
+ * @param string $event
+ * @param EventManager $events
+ * @return callable[]
+ */
+ private function getArrayOfListenersForEvent($event, EventManager $events)
+ {
+ return iterator_to_array($this->getListenersForEvent($event, $events));
+ }
+
+ /**
+ * Generator for traversing listeners in priority order.
+ *
+ * @param array $listeners
+ * @param bool $withPriority When true, yields priority as key.
+ */
+ public function traverseListeners(array $queue, $withPriority = false)
+ {
+ krsort($queue, SORT_NUMERIC);
+
+ foreach ($queue as $priority => $listeners) {
+ $priority = (int) $priority;
+ foreach ($listeners as $listener) {
+ if ($withPriority) {
+ yield $priority => $listener;
+ } else {
+ yield $listener;
+ }
+ }
+ }
+ }
+}
diff --git a/test/Test/EventListenerIntrospectionTraitTest.php b/test/Test/EventListenerIntrospectionTraitTest.php
new file mode 100644
index 0000000..ec27fa2
--- /dev/null
+++ b/test/Test/EventListenerIntrospectionTraitTest.php
@@ -0,0 +1,199 @@
+events = new EventManager();
+ }
+
+ public function testGetEventsFromEventManagerReturnsEventList()
+ {
+ // @codingStandardsIgnoreStart
+ $this->events->attach('foo', function ($e) {});
+ $this->events->attach('bar', function ($e) {});
+ $this->events->attach('baz', function ($e) {});
+ // @codingStandardsIgnoreEnd
+
+ $this->assertEquals(['foo', 'bar', 'baz'], $this->getEventsFromEventManager($this->events));
+ }
+
+ public function testGetListenersForEventReturnsIteratorOfListenersForEventInPriorityOrder()
+ {
+ // @codingStandardsIgnoreStart
+ $callback1 = function ($e) {};
+ $callback2 = function ($e) {};
+ $callback3 = function ($e) {};
+ $callback4 = function ($e) {};
+ $callback5 = function ($e) {};
+ // @codingStandardsIgnoreEnd
+
+ $this->events->attach('foo', $callback5, 1);
+ $this->events->attach('foo', $callback1, 2);
+ $this->events->attach('foo', $callback4, 3);
+ $this->events->attach('foo', $callback3, 4);
+ $this->events->attach('foo', $callback2, 5);
+
+ $listeners = $this->getListenersForEvent('foo', $this->events);
+ $this->assertInstanceOf(Traversable::class, $listeners);
+ $listeners = iterator_to_array($listeners);
+
+ $this->assertEquals([
+ $callback5,
+ $callback1,
+ $callback4,
+ $callback3,
+ $callback2,
+ ], $listeners);
+ }
+
+ public function testGetListenersForEventReturnsIteratorOfListenersInAttachmentOrderWhenSamePriority()
+ {
+ // @codingStandardsIgnoreStart
+ $callback1 = function ($e) {};
+ $callback2 = function ($e) {};
+ $callback3 = function ($e) {};
+ $callback4 = function ($e) {};
+ $callback5 = function ($e) {};
+ // @codingStandardsIgnoreEnd
+
+ $this->events->attach('foo', $callback5);
+ $this->events->attach('foo', $callback1);
+ $this->events->attach('foo', $callback4);
+ $this->events->attach('foo', $callback3);
+ $this->events->attach('foo', $callback2);
+
+ $listeners = $this->getListenersForEvent('foo', $this->events);
+ $this->assertInstanceOf(Traversable::class, $listeners);
+ $listeners = iterator_to_array($listeners);
+
+ $this->assertEquals([
+ $callback5,
+ $callback1,
+ $callback4,
+ $callback3,
+ $callback2,
+ ], $listeners);
+ }
+
+ public function testGetListenersForEventCanReturnPriorityKeysWhenRequested()
+ {
+ // @codingStandardsIgnoreStart
+ $callback1 = function ($e) {};
+ $callback2 = function ($e) {};
+ $callback3 = function ($e) {};
+ $callback4 = function ($e) {};
+ $callback5 = function ($e) {};
+ // @codingStandardsIgnoreEnd
+
+ $this->events->attach('foo', $callback5, 1);
+ $this->events->attach('foo', $callback1, 2);
+ $this->events->attach('foo', $callback4, 3);
+ $this->events->attach('foo', $callback3, 4);
+ $this->events->attach('foo', $callback2, 5);
+
+ $listeners = $this->getListenersForEvent('foo', $this->events, true);
+ $this->assertInstanceOf(Traversable::class, $listeners);
+ $listeners = iterator_to_array($listeners);
+
+ $this->assertEquals([
+ 1 => $callback5,
+ 2 => $callback1,
+ 3 => $callback4,
+ 4 => $callback3,
+ 5 => $callback2,
+ ], $listeners);
+ }
+
+ public function testGetArrayOfListenersForEventReturnsArrayOfListenersInPriorityOrder()
+ {
+ // @codingStandardsIgnoreStart
+ $callback1 = function ($e) {};
+ $callback2 = function ($e) {};
+ $callback3 = function ($e) {};
+ $callback4 = function ($e) {};
+ $callback5 = function ($e) {};
+ // @codingStandardsIgnoreEnd
+
+ $this->events->attach('foo', $callback5, 1);
+ $this->events->attach('foo', $callback1, 1);
+ $this->events->attach('foo', $callback4, 3);
+ $this->events->attach('foo', $callback3, 2);
+ $this->events->attach('foo', $callback2, 2);
+
+ $listeners = $this->getArrayOfListenersForEvent('foo', $this->events);
+ $this->assertInternalType('array', $listeners);
+
+ $this->assertEquals([
+ $callback5,
+ $callback1,
+ $callback3,
+ $callback2,
+ $callback4,
+ ], $listeners);
+ }
+
+ public function testAssertListenerAtPriorityPassesWhenListenerIsFound()
+ {
+ // @codingStandardsIgnoreStart
+ $callback = function ($e) {};
+ // @codingStandardsIgnoreEnd
+
+ $this->events->attach('foo', $callback, 7);
+
+ $this->assertListenerAtPriority($callback, 7, 'foo', $this->events);
+ }
+
+ public function testAssertListenerAtPriorityFailsWhenListenerIsNotFound()
+ {
+ // @codingStandardsIgnoreStart
+ $event = 'foo';
+ $listener = function ($e) {};
+ $priority = 7;
+ $this->events->attach($event, $listener, $priority);
+
+ $alternate = function ($e) {};
+
+ $permutations = [
+ 'different-listener' => ['listener' => $alternate, 'priority' => $priority, 'event' => $event],
+ 'different-priority' => ['listener' => $listener, 'priority' => $priority + 1, 'event' => $event],
+ 'different-event' => ['listener' => $listener, 'priority' => $priority, 'event' => $event . '-FOO'],
+ ];
+ // @codingStandardsIgnoreEnd
+
+ foreach ($permutations as $case => $arguments) {
+ try {
+ $this->assertListenerAtPriority(
+ $arguments['listener'],
+ $arguments['priority'],
+ $arguments['event'],
+ $this->events
+ );
+ $this->fail('assertListenerAtPriority assertion had a false positive for case ' . $case);
+ } catch (ExpectationFailedException $e) {
+ $this->assertContains(sprintf(
+ 'Listener not found for event "%s" and priority %d',
+ $arguments['event'],
+ $arguments['priority']
+ ), $e->getMessage(), sprintf('Assertion failure message was unexpected: %s', $e->getMessage()));
+ }
+ }
+ }
+}