--- /dev/null
+<?php
+
+namespace Drupal\DrupalExtension\Context\Environment\Reader;
+
+use Behat\Behat\Context\Environment\UninitializedContextEnvironment;
+use Behat\Behat\Context\Environment\ContextEnvironment;
+use Behat\Behat\Context\Reader\ContextReader;
+use Behat\Testwork\Call\Callee;
+use Behat\Testwork\Environment\Environment;
+use Behat\Testwork\Environment\Exception\EnvironmentReadException;
+use Behat\Testwork\Environment\Reader\EnvironmentReader;
+
+use Drupal\DrupalDriverManager;
+use Drupal\Driver\SubDriverFinderInterface;
+
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+
+/**
+ * Read in additional contexts provided by core and contrib.
+ */
+final class Reader implements EnvironmentReader {
+
+ /**
+ * @var ContextReader[]
+ */
+ private $contextReaders = array();
+
+ /**
+ * Drupal driver manager.
+ *
+ * @var \Drupal\DrupalDriverManager
+ */
+ private $drupal;
+
+ /**
+ * Configuration parameters for this suite.
+ */
+ private $parameters;
+
+ /**
+ * Statically cached lists of subcontexts by path.
+ *
+ * @var array
+ */
+ static protected $subContexts;
+
+ /**
+ * Register the Drupal driver manager.
+ */
+ public function __construct(DrupalDriverManager $drupal, array $parameters) {
+ $this->drupal = $drupal;
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * Registers context loader.
+ *
+ * @param ContextReader $contextReader
+ */
+ public function registerContextReader(ContextReader $contextReader) {
+ $this->contextReaders[] = $contextReader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsEnvironment(Environment $environment) {
+ return $environment instanceof ContextEnvironment;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readEnvironmentCallees(Environment $environment) {
+
+ if (!$environment instanceof ContextEnvironment) {
+ throw new EnvironmentReadException(sprintf(
+ 'ContextEnvironmentReader does not support `%s` environment.',
+ get_class($environment)
+ ), $environment);
+ }
+
+ $callees = array();
+ if (!$environment instanceof UninitializedContextEnvironment) {
+ return $callees;
+ }
+
+ $contextClasses = $this->findSubContextClasses();
+
+ foreach ($contextClasses as $contextClass) {
+ // When executing test scenarios with an examples table the registering of
+ // contexts is handled differently in newer version of Behat. Starting
+ // with Behat 3.2.0 the contexts are already registered, and the callees
+ // are returned by the default reader.
+ // Work around this and provide compatibility with Behat 3.1.0 as well as
+ // 3.2.0 and higher by checking if the class already exists before
+ // registering it and returning the callees.
+ // @see https://github.com/Behat/Behat/issues/758
+ if (!$environment->hasContextClass($contextClass)) {
+ $callees = array_merge(
+ $callees,
+ $this->readContextCallees($environment, $contextClass)
+ );
+
+ // Register context.
+ $environment->registerContextClass($contextClass, array($this->drupal));
+ }
+ }
+
+ return $callees;
+ }
+
+ /**
+ * Reads callees from a specific suite's context.
+ *
+ * @param ContextEnvironment $environment
+ * @param string $contextClass
+ *
+ * @return Callee[]
+ */
+ private function readContextCallees(ContextEnvironment $environment, $contextClass)
+ {
+ $callees = array();
+ foreach ($this->contextReaders as $loader) {
+ $callees = array_merge(
+ $callees,
+ $loader->readContextCallees($environment, $contextClass)
+ );
+ }
+
+ return $callees;
+ }
+
+ /**
+ * Finds and loads available subcontext classes.
+ */
+ private function findSubContextClasses() {
+ $class_names = array();
+
+ // Initialize any available sub-contexts.
+ if (isset($this->parameters['subcontexts'])) {
+ $paths = array();
+ // Drivers may specify paths to subcontexts.
+ if ($this->parameters['subcontexts']['autoload']) {
+ foreach ($this->drupal->getDrivers() as $name => $driver) {
+ if ($driver instanceof SubDriverFinderInterface) {
+ $paths += $driver->getSubDriverPaths();
+ }
+ }
+ }
+
+ // Additional subcontext locations may be specified manually in behat.yml.
+ if (isset($this->parameters['subcontexts']['paths'])) {
+ $paths = array_merge($paths, $this->parameters['subcontexts']['paths']);
+ }
+
+ // Load each class.
+ foreach ($paths as $path) {
+ if ($subcontexts = $this->findAvailableSubContexts($path)) {
+ $this->loadSubContexts($subcontexts);
+ }
+ }
+
+ // Find all subcontexts, excluding abstract base classes.
+ $classes = get_declared_classes();
+ foreach ($classes as $class) {
+ $reflect = new \ReflectionClass($class);
+ if (!$reflect->isAbstract() && $reflect->implementsInterface('Drupal\DrupalExtension\Context\DrupalSubContextInterface')) {
+ $class_names[] = $class;
+ }
+ }
+
+ }
+
+ return $class_names;
+ }
+
+ /**
+ * Find Sub-contexts matching a given pattern located at the passed path.
+ *
+ * @param string $path
+ * Absolute path to the directory to search for sub-contexts.
+ * @param string $pattern
+ * File pattern to match. Defaults to `/^.+\.behat\.inc/i`.
+ *
+ * @return array
+ * An array of paths.
+ */
+ private function findAvailableSubContexts($path, $pattern = '/^.+\.behat\.inc/i') {
+
+ if (isset(static::$subContexts[$pattern][$path])) {
+ return static::$subContexts[$pattern][$path];
+ }
+
+ static::$subContexts[$pattern][$path] = array();
+
+ $fileIterator = new RegexIterator(
+ new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path)
+ ), $pattern,
+ RegexIterator::MATCH
+ );
+ foreach ($fileIterator as $found) {
+ static::$subContexts[$pattern][$path][$found->getRealPath()] = $found->getFileName();
+ }
+
+ return static::$subContexts[$pattern][$path];
+ }
+
+ /**
+ * Load each subcontext file.
+ *
+ * @param array $subcontexts
+ * An array of files to include.
+ */
+ private function loadSubContexts($subcontexts) {
+ foreach ($subcontexts as $path => $subcontext) {
+ if (!file_exists($path)) {
+ throw new \RuntimeException(sprintf('Subcontext path %s path does not exist.', $path));
+ }
+
+ // Load file.
+ require_once $path;
+ }
+ }
+
+}