0c729aebce2469f28442d1adf3b6660dd24b3a61
[moodle.git] / lib / behat / classes / behat_config_util.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Utils to set Behat config
19  *
20  * @package    core
21  * @copyright  2016 Rajesh Taneja
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once(__DIR__ . '/../lib.php');
28 require_once(__DIR__ . '/behat_command.php');
29 require_once(__DIR__ . '/../../testing/classes/tests_finder.php');
31 /**
32  * Behat configuration manager
33  *
34  * Creates/updates Behat config files getting tests
35  * and steps from Moodle codebase
36  *
37  * @package    core
38  * @copyright  2016 Rajesh Taneja
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class behat_config_util {
43     /**
44      * @var array list of features in core.
45      */
46     private $features;
48     /**
49      * @var array list of contexts in core.
50      */
51     private $contexts;
53     /**
54      * @var array list of theme specific contexts.
55      */
56     private $themecontexts;
58     /**
59      * @var array list of all contexts in theme suite.
60      */
61     private $themesuitecontexts;
63     /**
64      * @var array list of overridden theme contexts.
65      */
66     private $overriddenthemescontexts;
68     /**
69      * @var array list of components with tests.
70      */
71     private $componentswithtests;
73     /**
74      * @var array|string keep track of theme to return suite with all core features included or not.
75      */
76     private $themesuitewithallfeatures = array();
78     /**
79      * @var string filter features which have tags.
80      */
81     private $tags = '';
83     /**
84      * @var int number of parallel runs.
85      */
86     private $parallelruns = 0;
88     /**
89      * @var int current run.
90      */
91     private $currentrun = 0;
93     /**
94      * @var string used to specify if behat should be initialised with all themes.
95      */
96     const ALL_THEMES_TO_RUN = 'ALL';
98     /**
99      * Set value for theme suite to include all core features. This should be used if your want all core features to be
100      * run with theme.
101      *
102      * @param bool $themetoset
103      */
104     public function set_theme_suite_to_include_core_features($themetoset) {
105         // If no value passed to --add-core-features-to-theme or ALL is passed, then set core features for all themes.
106         if (!empty($themetoset)) {
107             if (is_number($themetoset) || is_bool($themetoset) || (self::ALL_THEMES_TO_RUN === strtoupper($themetoset))) {
108                 $this->themesuitewithallfeatures = self::ALL_THEMES_TO_RUN;
109             } else {
110                 $this->themesuitewithallfeatures = explode(',', $themetoset);
111                 $this->themesuitewithallfeatures = array_map('trim', $this->themesuitewithallfeatures);
112             }
113         }
114     }
116     /**
117      * Set the value for tags, so features which are returned will be using filtered by this.
118      *
119      * @param string $tags
120      */
121     public function set_tag_for_feature_filter($tags) {
122         $this->tags = $tags;
123     }
125     /**
126      * Set parallel run to be used for generating config.
127      *
128      * @param int $parallelruns number of parallel runs.
129      * @param int $currentrun current run
130      */
131     public function set_parallel_run($parallelruns, $currentrun) {
133         if ($parallelruns < $currentrun) {
134             behat_error(BEHAT_EXITCODE_REQUIREMENT,
135                 'Parallel runs('.$parallelruns.') should be more then current run('.$currentrun.')');
136         }
138         $this->parallelruns = $parallelruns;
139         $this->currentrun = $currentrun;
140     }
142     /**
143      * Return parallel runs
144      *
145      * @return int number of parallel runs.
146      */
147     public function get_number_of_parallel_run() {
148         // Get number of parallel runs if not passed.
149         if (empty($this->parallelruns) && ($this->parallelruns !== false)) {
150             $this->parallelruns = behat_config_manager::get_behat_run_config_value('parallel');
151         }
153         return $this->parallelruns;
154     }
156     /**
157      * Return current run
158      *
159      * @return int current run.
160      */
161     public function get_current_run() {
162         global $CFG;
164         // Get number of parallel runs if not passed.
165         if (empty($this->currentrun) && ($this->currentrun !== false) && !empty($CFG->behatrunprocess)) {
166             $this->currentrun = $CFG->behatrunprocess;
167         }
169         return $this->currentrun;
170     }
172     /**
173      * Return list of features.
174      *
175      * @param string $tags tags.
176      * @return array
177      */
178     public function get_components_features($tags = '') {
179         global $CFG;
181         // If we already have a list created then just return that, as it's up-to-date.
182         // If tags are passed then it's a new filter of features we need.
183         if (!empty($this->features) && empty($tags)) {
184             return $this->features;
185         }
187         // Gets all the components with features.
188         $features = array();
189         $featurespaths = array();
190         $components = $this->get_components_with_tests();
192         if ($components) {
193             foreach ($components as $componentname => $path) {
194                 $path = $this->clean_path($path) . self::get_behat_tests_path();
195                 if (empty($featurespaths[$path]) && file_exists($path)) {
196                     list($key, $featurepath) = $this->get_clean_feature_key_and_path($path);
197                     $featurespaths[$key] = $featurepath;
198                 }
199             }
200             foreach ($featurespaths as $path) {
201                 $additional = glob("$path/*.feature");
203                 $additionalfeatures = array();
204                 foreach ($additional as $featurepath) {
205                     list($key, $path) = $this->get_clean_feature_key_and_path($featurepath);
206                     $additionalfeatures[$key] = $path;
207                 }
209                 $features = array_merge($features, $additionalfeatures);
210             }
211         }
213         // Optionally include features from additional directories.
214         if (!empty($CFG->behat_additionalfeatures)) {
215             $additional = array_map("realpath", $CFG->behat_additionalfeatures);
216             $additionalfeatures = array();
217             foreach ($additional as $featurepath) {
218                 list($key, $path) = $this->get_clean_feature_key_and_path($featurepath);
219                 $additionalfeatures[$key] = $path;
220             }
221             $features = array_merge($features, $additionalfeatures);
222         }
224         // Sanitize feature key.
225         $cleanfeatures = array();
226         foreach ($features as $featurepath) {
227             list($key, $path) = $this->get_clean_feature_key_and_path($featurepath);
228             $cleanfeatures[$key] = $path;
229         }
231         // Sort feature list.
232         ksort($cleanfeatures);
234         $this->features = $cleanfeatures;
236         // If tags are passed then filter features which has sepecified tags.
237         if (!empty($tags)) {
238             $cleanfeatures = $this->filtered_features_with_tags($cleanfeatures, $tags);
239         }
241         return $cleanfeatures;
242     }
244     /**
245      * Return feature key for featurepath
246      *
247      * @param string $featurepath
248      * @return array key and featurepath.
249      */
250     public function get_clean_feature_key_and_path($featurepath) {
251         global $CFG;
253         // Fix directory path.
254         $featurepath = testing_cli_fix_directory_separator($featurepath);
255         $dirroot = testing_cli_fix_directory_separator($CFG->dirroot . DIRECTORY_SEPARATOR);
257         $key = basename($featurepath, '.feature');
259         // Get relative path.
260         $featuredirname = str_replace($dirroot , '', $featurepath);
261         // Get 5 levels of feature path to ensure we have a unique key.
262         for ($i = 0; $i < 5; $i++) {
263             if (($featuredirname = dirname($featuredirname)) && $featuredirname !== '.') {
264                 if ($basename = basename($featuredirname)) {
265                     $key .= '_' . $basename;
266                 }
267             }
268         }
270         return array($key, $featurepath);
271     }
273     /**
274      * Get component contexts.
275      *
276      * @param string $component component name.
277      * @return array
278      */
279     private function get_component_contexts($component) {
281         if (empty($component)) {
282             return $this->contexts;
283         }
285         $componentcontexts = array();
286         foreach ($this->contexts as $key => $path) {
287             if ($component == '' || $component === $key) {
288                 $componentcontexts[$key] = $path;
289             }
290         }
292         return $componentcontexts;
293     }
295     /**
296      * Gets the list of Moodle behat contexts
297      *
298      * Class name as a key and the filepath as value
299      *
300      * Externalized from update_config_file() to use
301      * it from the steps definitions web interface
302      *
303      * @param  string $component Restricts the obtained steps definitions to the specified component
304      * @return array
305      */
306     public function get_components_contexts($component = '') {
308         // If we already have a list created then just return that, as it's up-to-date.
309         if (!empty($this->contexts)) {
310             return $this->get_component_contexts($component);
311         }
313         $components = $this->get_components_with_tests();
315         $this->contexts = array();
316         foreach ($components as $componentname => $componentpath) {
317             $componentpath = self::clean_path($componentpath);
319             if (!file_exists($componentpath . self::get_behat_tests_path())) {
320                 continue;
321             }
322             $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
323             $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
325             // All behat_*.php inside self::get_behat_tests_path() are added as steps definitions files.
326             foreach ($regite as $file) {
327                 $key = $file->getBasename('.php');
328                 $this->contexts[$key] = $file->getPathname();
329             }
330         }
332         // Sort contexts with there name.
333         ksort($this->contexts);
335         return $this->get_component_contexts($component);
336     }
338     /**
339      * Behat config file specifing the main context class,
340      * the required Behat extensions and Moodle test wwwroot.
341      *
342      * @param array $features The system feature files
343      * @param array $contexts The system steps definitions
344      * @param string $tags filter features with specified tags.
345      * @param int $parallelruns number of parallel runs.
346      * @param int $currentrun current run for which config file is needed.
347      * @return string
348      */
349     public function get_config_file_contents($features = '', $contexts = '', $tags = '', $parallelruns = 0, $currentrun = 0) {
350         global $CFG;
352         // Set current run and parallel run.
353         if (!empty($parallelruns) && !empty($currentrun)) {
354             $this->set_parallel_run($parallelruns, $currentrun);
355         }
357         // If tags defined then use them. This is for BC.
358         if (!empty($tags)) {
359             $this->set_tag_for_feature_filter($tags);
360         }
362         // If features not passed then get it. Empty array means we don't need to include features.
363         if (empty($features) && !is_array($features)) {
364             $features = $this->get_components_features();
365         } else {
366             $this->features = $features;
367         }
369         // If stepdefinitions not passed then get the list.
370         if (empty($contexts)) {
371             $this->get_components_contexts();
372         } else {
373             $this->contexts = $contexts;
374         }
376         // We require here when we are sure behat dependencies are available.
377         require_once($CFG->dirroot . '/vendor/autoload.php');
379         $config = $this->build_config();
381         $config = $this->merge_behat_config($config);
383         $config = $this->merge_behat_profiles($config);
385         // Return config array for phpunit, so it can be tested.
386         if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
387             return $config;
388         }
390         return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
391     }
393     /**
394      * Search feature files for set of tags.
395      *
396      * @param array $features set of feature files.
397      * @param string $tags list of tags (currently support && only.)
398      * @return array filtered list of feature files with tags.
399      */
400     public function filtered_features_with_tags($features = '', $tags = '') {
402         // This is for BC. Features if not passed then we already have a list in this object.
403         if (empty($features)) {
404             $features = $this->features;
405         }
407         // If no tags defined then return full list.
408         if (empty($tags) && empty($this->tags)) {
409             return $features;
410         }
412         // If no tags passed by the caller, then it's already set.
413         if (empty($tags)) {
414             $tags = $this->tags;
415         }
417         $newfeaturelist = array();
418         // Split tags in and and or.
419         $tags = explode('&&', $tags);
420         $andtags = array();
421         $ortags = array();
422         foreach ($tags as $tag) {
423             // Explode all tags seperated by , and add it to ortags.
424             $ortags = array_merge($ortags, explode(',', $tag));
425             // And tags will be the first one before comma(,).
426             $andtags[] = preg_replace('/,.*/', '', $tag);
427         }
429         foreach ($features as $key => $featurefile) {
430             $contents = file_get_contents($featurefile);
431             $includefeature = true;
432             foreach ($andtags as $tag) {
433                 // If negitive tag, then ensure it don't exist.
434                 if (strpos($tag, '~') !== false) {
435                     $tag = substr($tag, 1);
436                     if ($contents && strpos($contents, $tag) !== false) {
437                         $includefeature = false;
438                         break;
439                     }
440                 } else if ($contents && strpos($contents, $tag) === false) {
441                     $includefeature = false;
442                     break;
443                 }
444             }
446             // If feature not included then check or tags.
447             if (!$includefeature && !empty($ortags)) {
448                 foreach ($ortags as $tag) {
449                     if ($contents && (strpos($tag, '~') === false) && (strpos($contents, $tag) !== false)) {
450                         $includefeature = true;
451                         break;
452                     }
453                 }
454             }
456             if ($includefeature) {
457                 $newfeaturelist[$key] = $featurefile;
458             }
459         }
460         return $newfeaturelist;
461     }
463     /**
464      * Build config for behat.yml.
465      *
466      * @param int $parallelruns how many parallel runs feature needs to be divided.
467      * @param int $currentrun current run for which features should be returned.
468      * @return array
469      */
470     protected function build_config($parallelruns = 0, $currentrun = 0) {
471         global $CFG;
473         if (!empty($parallelruns) && !empty($currentrun)) {
474             $this->set_parallel_run($parallelruns, $currentrun);
475         } else {
476             $currentrun = $this->get_current_run();
477             $parallelruns = $this->get_number_of_parallel_run();
478         }
480         $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub');
481         // If parallel run, then set wd_host if specified.
482         if (!empty($currentrun) && !empty($parallelruns)) {
483             // Set proper selenium2 wd_host if defined.
484             if (!empty($CFG->behat_parallel_run[$currentrun - 1]['wd_host'])) {
485                 $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$currentrun - 1]['wd_host']);
486             }
487         }
489         // It is possible that it has no value as we don't require a full behat setup to list the step definitions.
490         if (empty($CFG->behat_wwwroot)) {
491             $CFG->behat_wwwroot = 'http://itwillnotbeused.com';
492         }
494         $suites = $this->get_behat_suites($parallelruns, $currentrun);
496         $overriddenthemescontexts = $this->get_overridden_theme_contexts();
497         if (!empty($overriddenthemescontexts)) {
498             $allcontexts = array_merge($this->contexts, $overriddenthemescontexts);
499         } else {
500             $allcontexts = $this->contexts;
501         }
503         // Remove selectors from step definitions.
504         $themes = $this->get_list_of_themes();
505         $selectortypes = ['named_partial', 'named_exact'];
506         foreach ($themes as $theme) {
507             foreach ($selectortypes as $selectortype) {
508                 // Don't include selector classes.
509                 $selectorclass = self::get_behat_theme_selector_override_classname($theme, $selectortype);
510                 if (isset($allcontexts[$selectorclass])) {
511                     unset($allcontexts[$selectorclass]);
512                 }
513             }
514         }
516         // Comments use black color, so failure path is not visible. Using color other then black/white is safer.
517         // https://github.com/Behat/Behat/pull/628.
518         $config = array(
519             'default' => array(
520                 'formatters' => array(
521                     'moodle_progress' => array(
522                         'output_styles' => array(
523                             'comment' => array('magenta'))
524                     )
525                 ),
526                 'suites' => $suites,
527                 'extensions' => array(
528                     'Behat\MinkExtension' => array(
529                         'base_url' => $CFG->behat_wwwroot,
530                         'goutte' => null,
531                         'selenium2' => $selenium2wdhost
532                     ),
533                     'Moodle\BehatExtension' => array(
534                         'moodledirroot' => $CFG->dirroot,
535                         'steps_definitions' => $allcontexts,
536                     )
537                 )
538             )
539         );
541         return $config;
542     }
544     /**
545      * Divide features between the runs and return list.
546      *
547      * @param array $features list of features to be divided.
548      * @param int $parallelruns how many parallel runs feature needs to be divided.
549      * @param int $currentrun current run for which features should be returned.
550      * @return array
551      */
552     protected function get_features_for_the_run($features, $parallelruns, $currentrun) {
554         // If no features are passed then just return.
555         if (empty($features)) {
556             return $features;
557         }
559         $allocatedfeatures = $features;
561         // If parallel run, then only divide features.
562         if (!empty($currentrun) && !empty($parallelruns)) {
564             $featurestodivide['withtags'] = $features;
565             $allocatedfeatures = array();
567             // If tags are set then split features with tags first.
568             if (!empty($this->tags)) {
569                 $featurestodivide['withtags'] = $this->filtered_features_with_tags($features);
570                 $featurestodivide['withouttags'] = $this->remove_blacklisted_features_from_list($features,
571                     $featurestodivide['withtags']);
572             }
574             // Attempt to split into weighted buckets using timing information, if available.
575             foreach ($featurestodivide as $tagfeatures) {
576                 if ($alloc = $this->profile_guided_allocate($tagfeatures, max(1, $parallelruns), $currentrun)) {
577                     $allocatedfeatures = array_merge($allocatedfeatures, $alloc);
578                 } else {
579                     // Divide the list of feature files amongst the parallel runners.
580                     // Pull out the features for just this worker.
581                     if (count($tagfeatures)) {
582                         $splitfeatures = array_chunk($tagfeatures, ceil(count($tagfeatures) / max(1, $parallelruns)));
584                         // Check if there is any feature file for this process.
585                         if (!empty($splitfeatures[$currentrun - 1])) {
586                             $allocatedfeatures = array_merge($allocatedfeatures, $splitfeatures[$currentrun - 1]);
587                         }
588                     }
589                 }
590             }
591         }
593         return $allocatedfeatures;
594     }
596     /**
597      * Parse $CFG->behat_profile and return the array with required config structure for behat.yml.
598      *
599      * $CFG->behat_profiles = array(
600      *     'profile' = array(
601      *         'browser' => 'firefox',
602      *         'tags' => '@javascript',
603      *         'wd_host' => 'http://127.0.0.1:4444/wd/hub',
604      *         'capabilities' => array(
605      *             'platform' => 'Linux',
606      *             'version' => 44
607      *         )
608      *     )
609      * );
610      *
611      * @param string $profile profile name
612      * @param array $values values for profile.
613      * @return array
614      */
615     protected function get_behat_profile($profile, $values) {
616         // Values should be an array.
617         if (!is_array($values)) {
618             return array();
619         }
621         // Check suite values.
622         $behatprofilesuites = array();
624         // Automatically set tags information to skip app testing if necessary. We skip app testing
625         // if the browser is not Chrome. (Note: We also skip if it's not configured, but that is
626         // done on the theme/suite level.)
627         if (empty($values['browser']) || $values['browser'] !== 'chrome') {
628             if (!empty($values['tags'])) {
629                 $values['tags'] .= ' && ~@app';
630             } else {
631                 $values['tags'] = '~@app';
632             }
633         }
635         // Automatically add Chrome command line option to skip the prompt about allowing file
636         // storage - needed for mobile app testing (won't hurt for everything else either).
637         if (!empty($values['browser']) && $values['browser'] === 'chrome') {
638             if (!isset($values['capabilities'])) {
639                 $values['capabilities'] = [];
640             }
641             if (!isset($values['capabilities']['chrome'])) {
642                 $values['capabilities']['chrome'] = [];
643             }
644             if (!isset($values['capabilities']['chrome']['switches'])) {
645                 $values['capabilities']['chrome']['switches'] = [];
646             }
647             $values['capabilities']['chrome']['switches'][] = '--unlimited-storage';
649             // If the mobile app is enabled, check its version and add appropriate tags.
650             if ($mobiletags = $this->get_mobile_version_tags()) {
651                 if (!empty($values['tags'])) {
652                     $values['tags'] .= ' && ' . $mobiletags;
653                 } else {
654                     $values['tags'] = $mobiletags;
655                 }
656             }
657         }
659         // Fill tags information.
660         if (isset($values['tags'])) {
661             $behatprofilesuites = array(
662                 'suites' => array(
663                     'default' => array(
664                         'filters' => array(
665                             'tags' => $values['tags'],
666                         )
667                     )
668                 )
669             );
670         }
672         // Selenium2 config values.
673         $behatprofileextension = array();
674         $seleniumconfig = array();
675         if (isset($values['browser'])) {
676             $seleniumconfig['browser'] = $values['browser'];
677         }
678         if (isset($values['wd_host'])) {
679             $seleniumconfig['wd_host'] = $values['wd_host'];
680         }
681         if (isset($values['capabilities'])) {
682             $seleniumconfig['capabilities'] = $values['capabilities'];
683         }
684         if (!empty($seleniumconfig)) {
685             $behatprofileextension = array(
686                 'extensions' => array(
687                     'Behat\MinkExtension' => array(
688                         'selenium2' => $seleniumconfig,
689                     )
690                 )
691             );
692         }
694         return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
695     }
697     /**
698      * Gets version tags to use for the mobile app.
699      *
700      * This is based on the current mobile app version (from its package.json) and all known
701      * mobile app versions (based on the list appversions.json in the lib/behat directory).
702      *
703      * @return string List of tags or '' if not supporting mobile
704      */
705     protected function get_mobile_version_tags() : string {
706         global $CFG;
708         if (empty($CFG->behat_ionic_dirroot)) {
709             return '';
710         }
712         // Get app version.
713         $jsonpath = $CFG->behat_ionic_dirroot . '/package.json';
714         $json = @file_get_contents($jsonpath);
715         if (!$json) {
716             throw new coding_exception('Unable to load app version from ' . $jsonpath);
717         }
718         $package = json_decode($json);
719         if ($package === null) {
720             throw new coding_exception('Invalid app package data in ' . $jsonpath);
721         }
722         $installedversion = $package->version;
724         // Read all feature files to check which mobile tags are used. (Note: This could be cached
725         // but ideally, it is the sort of thing that really ought to be refreshed by doing a new
726         // Behat init. Also, at time of coding it only takes 0.3 seconds and only if app enabled.)
727         $usedtags = [];
728         foreach ($this->features as $filepath) {
729             $feature = file_get_contents($filepath);
730             // This may incorrectly detect versions used e.g. in a comment or something, but it
731             // doesn't do much harm if we have extra ones.
732             if (preg_match_all('~@app_(?:from|upto)(?:[0-9]+(?:\.[0-9]+)*)~', $feature, $matches)) {
733                 foreach ($matches[0] as $tag) {
734                     // Store as key in array so we don't get duplicates.
735                     $usedtags[$tag] = true;
736                 }
737             }
738         }
740         // Set up relevant tags for each version.
741         $tags = [];
742         foreach ($usedtags as $usedtag => $ignored) {
743             if (!preg_match('~^@app_(from|upto)([0-9]+(?:\.[0-9]+)*)$~', $usedtag, $matches)) {
744                 throw new coding_exception('Unexpected tag format');
745             }
746             $direction = $matches[1];
747             $version = $matches[2];
749             switch (version_compare($installedversion, $version)) {
750                 case -1:
751                     // Installed version OLDER than the one being considered, so do not
752                     // include any scenarios that only run from the considered version up.
753                     if ($direction === 'from') {
754                         $tags[] = '~app_from' . $version;
755                     }
756                     break;
758                 case 0:
759                     // Installed version EQUAL to the one being considered - no tags need
760                     // excluding.
761                     break;
763                 case 1:
764                     // Installed version NEWER than the one being considered, so do not
765                     // include any scenarios that only run up to that version.
766                     if ($direction === 'upto') {
767                         $tags[] = '~app_upto' . $version;
768                     }
769                     break;
770             }
771         }
772         return join(' && ', $tags);
773     }
775     /**
776      * Attempt to split feature list into fairish buckets using timing information, if available.
777      * Simply add each one to lightest buckets until all files allocated.
778      * PGA = Profile Guided Allocation. I made it up just now.
779      * CAUTION: workers must agree on allocation, do not be random anywhere!
780      *
781      * @param array $features Behat feature files array
782      * @param int $nbuckets Number of buckets to divide into
783      * @param int $instance Index number of this instance
784      * @return array|bool Feature files array, sorted into allocations
785      */
786     public function profile_guided_allocate($features, $nbuckets, $instance) {
788         // No profile guided allocation is required in phpunit.
789         if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
790             return false;
791         }
793         $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') &&
794         @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false;
796         if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) {
797             // No data available, fall back to relying on steps data.
798             $stepfile = "";
799             if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) {
800                 $stepfile = BEHAT_FEATURE_STEP_FILE;
801             }
802             // We should never get this. But in case we can't do this then fall back on simple splitting.
803             if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) {
804                 return false;
805             }
806         }
808         arsort($behattimingdata); // Ensure most expensive is first.
810         $realroot = realpath(__DIR__.'/../../../').'/';
811         $defaultweight = array_sum($behattimingdata) / count($behattimingdata);
812         $weights = array_fill(0, $nbuckets, 0);
813         $buckets = array_fill(0, $nbuckets, array());
814         $totalweight = 0;
816         // Re-key the features list to match timing data.
817         foreach ($features as $k => $file) {
818             $key = str_replace($realroot, '', $file);
819             $features[$key] = $file;
820             unset($features[$k]);
821             if (!isset($behattimingdata[$key])) {
822                 $behattimingdata[$key] = $defaultweight;
823             }
824         }
826         // Sort features by known weights; largest ones should be allocated first.
827         $behattimingorder = array();
828         foreach ($features as $key => $file) {
829             $behattimingorder[$key] = $behattimingdata[$key];
830         }
831         arsort($behattimingorder);
833         // Finally, add each feature one by one to the lightest bucket.
834         foreach ($behattimingorder as $key => $weight) {
835             $file = $features[$key];
836             $lightbucket = array_search(min($weights), $weights);
837             $weights[$lightbucket] += $weight;
838             $buckets[$lightbucket][] = $file;
839             $totalweight += $weight;
840         }
842         if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets
843                 && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) {
844             echo "Bucket weightings:\n";
845             foreach ($weights as $k => $weight) {
846                 echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL;
847             }
848         }
850         // Return the features for this worker.
851         return $buckets[$instance - 1];
852     }
854     /**
855      * Overrides default config with local config values
856      *
857      * array_merge does not merge completely the array's values
858      *
859      * @param mixed $config The node of the default config
860      * @param mixed $localconfig The node of the local config
861      * @return mixed The merge result
862      */
863     public function merge_config($config, $localconfig) {
865         if (!is_array($config) && !is_array($localconfig)) {
866             return $localconfig;
867         }
869         // Local overrides also deeper default values.
870         if (is_array($config) && !is_array($localconfig)) {
871             return $localconfig;
872         }
874         foreach ($localconfig as $key => $value) {
876             // If defaults are not as deep as local values let locals override.
877             if (!is_array($config)) {
878                 unset($config);
879             }
881             // Add the param if it doesn't exists or merge branches.
882             if (empty($config[$key])) {
883                 $config[$key] = $value;
884             } else {
885                 $config[$key] = $this->merge_config($config[$key], $localconfig[$key]);
886             }
887         }
889         return $config;
890     }
892     /**
893      * Merges $CFG->behat_config with the one passed.
894      *
895      * @param array $config existing config.
896      * @return array merged config with $CFG->behat_config
897      */
898     public function merge_behat_config($config) {
899         global $CFG;
901         // In case user defined overrides respect them over our default ones.
902         if (!empty($CFG->behat_config)) {
903             foreach ($CFG->behat_config as $profile => $values) {
904                 $config = $this->merge_config($config, $this->get_behat_config_for_profile($profile, $values));
905             }
906         }
908         return $config;
909     }
911     /**
912      * Parse $CFG->behat_config and return the array with required config structure for behat.yml
913      *
914      * @param string $profile profile name
915      * @param array $values values for profile
916      * @return array
917      */
918     public function get_behat_config_for_profile($profile, $values) {
919         // Only add profile which are compatible with Behat 3.x
920         // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs
921         // Like : rerun_cache etc.
922         if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) {
923             return array($profile => $values);
924         }
926         // Parse 2.5 format and get related values.
927         $oldconfigvalues = array();
928         if (isset($values['extensions']['Behat\MinkExtension\Extension'])) {
929             $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension'];
930             if (isset($extensionvalues['selenium2']['browser'])) {
931                 $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser'];
932             }
933             if (isset($extensionvalues['selenium2']['wd_host'])) {
934                 $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host'];
935             }
936             if (isset($extensionvalues['capabilities'])) {
937                 $oldconfigvalues['capabilities'] = $extensionvalues['capabilities'];
938             }
939         }
941         if (isset($values['filters']['tags'])) {
942             $oldconfigvalues['tags'] = $values['filters']['tags'];
943         }
945         if (!empty($oldconfigvalues)) {
946             behat_config_manager::$autoprofileconversion = true;
947             return $this->get_behat_profile($profile, $oldconfigvalues);
948         }
950         // If nothing set above then return empty array.
951         return array();
952     }
954     /**
955      * Merges $CFG->behat_profiles with the one passed.
956      *
957      * @param array $config existing config.
958      * @return array merged config with $CFG->behat_profiles
959      */
960     public function merge_behat_profiles($config) {
961         global $CFG;
963         // Check for Moodle custom ones.
964         if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) {
965             foreach ($CFG->behat_profiles as $profile => $values) {
966                 $config = $this->merge_config($config, $this->get_behat_profile($profile, $values));
967             }
968         }
970         return $config;
971     }
973     /**
974      * Cleans the path returned by get_components_with_tests() to standarize it
975      *
976      * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/
977      * @param string $path
978      * @return string The string without the last /tests part
979      */
980     public final function clean_path($path) {
982         $path = rtrim($path, DIRECTORY_SEPARATOR);
984         $parttoremove = DIRECTORY_SEPARATOR . 'tests';
986         $substr = substr($path, strlen($path) - strlen($parttoremove));
987         if ($substr == $parttoremove) {
988             $path = substr($path, 0, strlen($path) - strlen($parttoremove));
989         }
991         return rtrim($path, DIRECTORY_SEPARATOR);
992     }
994     /**
995      * The relative path where components stores their behat tests
996      *
997      * @return string
998      */
999     public static final function get_behat_tests_path() {
1000         return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
1001     }
1003     /**
1004      * Return context name of behat_theme selector to use.
1005      *
1006      * @param string $themename name of the theme.
1007      * @param string $selectortype The type of selector (partial or exact at this stage)
1008      * @param bool $includeclass if class should be included.
1009      * @return string
1010      */
1011     public static final function get_behat_theme_selector_override_classname($themename, $selectortype, $includeclass = false) {
1012         global $CFG;
1014         if ($selectortype !== 'named_partial' && $selectortype !== 'named_exact') {
1015             throw new coding_exception("Unknown selector override type '{$selectortype}'");
1016         }
1018         $overridebehatclassname = "behat_theme_{$themename}_behat_{$selectortype}_selectors";
1020         if ($includeclass) {
1021             $themeoverrideselector = $CFG->dirroot . DIRECTORY_SEPARATOR . 'theme' . DIRECTORY_SEPARATOR . $themename .
1022                 self::get_behat_tests_path() . DIRECTORY_SEPARATOR . $overridebehatclassname . '.php';
1024             if (file_exists($themeoverrideselector)) {
1025                 require_once($themeoverrideselector);
1026             }
1027         }
1029         return $overridebehatclassname;
1030     }
1032     /**
1033      * List of components which contain behat context or features.
1034      *
1035      * @return array
1036      */
1037     protected function get_components_with_tests() {
1038         if (empty($this->componentswithtests)) {
1039             $this->componentswithtests = tests_finder::get_components_with_tests('behat');
1040         }
1042         return $this->componentswithtests;
1043     }
1045     /**
1046      * Remove list of blacklisted features from the feature list.
1047      *
1048      * @param array $features list of original features.
1049      * @param array|string $blacklist list of features which needs to be removed.
1050      * @return array features - blacklisted features.
1051      */
1052     protected function remove_blacklisted_features_from_list($features, $blacklist) {
1054         // If no blacklist passed then return.
1055         if (empty($blacklist)) {
1056             return $features;
1057         }
1059         // If there is no feature in suite then just return what was passed.
1060         if (empty($features)) {
1061             return $features;
1062         }
1064         if (!is_array($blacklist)) {
1065             $blacklist = array($blacklist);
1066         }
1068         // Remove blacklisted features.
1069         foreach ($blacklist as $blacklistpath) {
1071             list($key, $featurepath) = $this->get_clean_feature_key_and_path($blacklistpath);
1073             if (isset($features[$key])) {
1074                 $features[$key] = null;
1075                 unset($features[$key]);
1076             } else {
1077                 $featurestocheck = $this->get_components_features();
1078                 if (!isset($featurestocheck[$key]) && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) {
1079                     behat_error(BEHAT_EXITCODE_REQUIREMENT, 'Blacklisted feature "' . $blacklistpath . '" not found.');
1080                 }
1081             }
1082         }
1084         return $features;
1085     }
1087     /**
1088      * Return list of behat suites. Multiple suites are returned if theme
1089      * overrides default step definitions/features.
1090      *
1091      * @param int $parallelruns number of parallel runs
1092      * @param int $currentrun current run.
1093      * @return array list of suites.
1094      */
1095     protected function get_behat_suites($parallelruns = 0, $currentrun = 0) {
1096         $features = $this->get_components_features();
1098         // Get number of parallel runs and current run.
1099         if (!empty($parallelruns) && !empty($currentrun)) {
1100             $this->set_parallel_run($parallelruns, $currentrun);
1101         } else {
1102             $parallelruns = $this->get_number_of_parallel_run();
1103             $currentrun = $this->get_current_run();;
1104         }
1106         $themefeatures = array();
1107         $themecontexts = array();
1109         $themes = $this->get_list_of_themes();
1111         // Create list of theme suite features and contexts.
1112         foreach ($themes as $theme) {
1113             // Get theme features.
1114             $themefeatures[$theme] = $this->get_behat_features_for_theme($theme);
1116             $themecontexts[$theme] = $this->get_behat_contexts_for_theme($theme);
1117         }
1119         // Remove list of theme features for default suite, as default suite should not run theme specific features.
1120         foreach ($themefeatures as $themename => $removethemefeatures) {
1121             if (!empty($removethemefeatures['features'])) {
1122                 $features = $this->remove_blacklisted_features_from_list($features, $removethemefeatures['features']);
1123             }
1124         }
1126         // Remove list of theme contexts form other suite contexts, as suite don't require other theme specific contexts.
1127         foreach ($themecontexts as $themename => $themecontext) {
1128             if (!empty($themecontext['contexts'])) {
1129                 foreach ($themecontext['contexts'] as $contextkey => $contextpath) {
1130                     // Remove theme specific contexts from other themes.
1131                     foreach ($themes as $currenttheme) {
1132                         if (($currenttheme != $themename) && isset($themecontexts[$currenttheme]['suitecontexts'][$contextkey])) {
1133                             unset($themecontexts[$currenttheme]['suitecontexts'][$contextkey]);
1134                         }
1135                     }
1136                 }
1137             }
1138         }
1140         // Set suite for each theme.
1141         $suites = array();
1142         foreach ($themes as $theme) {
1143             // Get list of features which will be included in theme.
1144             // If theme suite with all features or default theme, then we want all core features to be part of theme suite.
1145             if ((is_string($this->themesuitewithallfeatures) && ($this->themesuitewithallfeatures === self::ALL_THEMES_TO_RUN)) ||
1146                 in_array($theme, $this->themesuitewithallfeatures) || ($this->get_default_theme() === $theme)) {
1147                 // If there is no theme specific feature. Then it's just core features.
1148                 if (empty($themefeatures[$theme]['features'])) {
1149                     $themesuitefeatures = $features;
1150                 } else {
1151                     $themesuitefeatures = array_merge($features, $themefeatures[$theme]['features']);
1152                 }
1153             } else {
1154                 $themesuitefeatures = $themefeatures[$theme]['features'];
1155             }
1157             // Remove blacklisted features.
1158             $themesuitefeatures = $this->remove_blacklisted_features_from_list($themesuitefeatures,
1159                 $themefeatures[$theme]['blacklistfeatures']);
1161             // Return sub-set of features if parallel run.
1162             $themesuitefeatures = $this->get_features_for_the_run($themesuitefeatures, $parallelruns, $currentrun);
1164             // Default theme is part of default suite.
1165             if ($this->get_default_theme() === $theme) {
1166                 $suitename = 'default';
1167             } else {
1168                 $suitename = $theme;
1169             }
1171             // Add suite no matter what. If there is no feature in suite then it will just exist successfully with no
1172             // scenarios. But if we don't set this then the user has to know which run doesn't have suite and which run do.
1173             $suites = array_merge($suites, array(
1174                 $suitename => array(
1175                     'paths'    => array_values($themesuitefeatures),
1176                     'contexts' => array_keys($themecontexts[$theme]['suitecontexts']),
1177                 )
1178             ));
1179         }
1181         return $suites;
1182     }
1184     /**
1185      * Return name of default theme.
1186      *
1187      * @return string
1188      */
1189     protected function get_default_theme() {
1190         return theme_config::DEFAULT_THEME;
1191     }
1193     /**
1194      * Return list of themes which can be set in moodle.
1195      *
1196      * @return array list of themes with tests.
1197      */
1198     protected function get_list_of_themes() {
1199         $selectablethemes = array();
1201         // Get all themes installed on site.
1202         $themes = core_component::get_plugin_list('theme');
1203         ksort($themes);
1205         foreach ($themes as $themename => $themedir) {
1206             // Load the theme config.
1207             try {
1208                 $theme = theme_config::load($themename);
1209             } catch (Exception $e) {
1210                 // Bad theme, just skip it for now.
1211                 continue;
1212             }
1213             if ($themename !== $theme->name) {
1214                 // Obsoleted or broken theme, just skip for now.
1215                 continue;
1216             }
1217             if ($theme->hidefromselector) {
1218                 // The theme doesn't want to be shown in the theme selector and as theme
1219                 // designer mode is switched off we will respect that decision.
1220                 continue;
1221             }
1222             $selectablethemes[] = $themename;
1223         }
1225         return $selectablethemes;
1226     }
1228     /**
1229      * Return theme directory.
1230      *
1231      * @param string $themename
1232      * @return string theme directory
1233      */
1234     protected function get_theme_test_directory($themename) {
1235         global $CFG;
1237         $themetestdir = "/theme/" . $themename;
1239         return $CFG->dirroot . $themetestdir  . self::get_behat_tests_path();
1240     }
1242     /**
1243      * Returns all the directories having overridden tests.
1244      *
1245      * @param string $theme name of theme
1246      * @param string $testtype The kind of test we are looking for
1247      * @return array all directories having tests
1248      */
1249     protected function get_test_directories_overridden_for_theme($theme, $testtype) {
1250         global $CFG;
1252         $testtypes = array(
1253             'contexts' => '|behat_.*\.php$|',
1254             'features' => '|.*\.feature$|',
1255         );
1256         $themetestdirfullpath = $this->get_theme_test_directory($theme);
1258         // If test directory doesn't exist then return.
1259         if (!is_dir($themetestdirfullpath)) {
1260             return array();
1261         }
1263         $directoriestosearch = glob($themetestdirfullpath . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
1265         // Include theme directory to find tests.
1266         $dirs[realpath($themetestdirfullpath)] = trim(str_replace('/', '_', $themetestdirfullpath), '_');
1268         // Search for tests in valid directories.
1269         foreach ($directoriestosearch as $dir) {
1270             $dirite = new RecursiveDirectoryIterator($dir);
1271             $iteite = new RecursiveIteratorIterator($dirite);
1272             $regexp = $testtypes[$testtype];
1273             $regite = new RegexIterator($iteite, $regexp);
1274             foreach ($regite as $path => $element) {
1275                 $key = dirname($path);
1276                 $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
1277                 $dirs[$key] = $value;
1278             }
1279         }
1280         ksort($dirs);
1282         return array_flip($dirs);
1283     }
1285     /**
1286      * Return blacklisted contexts or features for a theme, as defined in blacklist.json.
1287      *
1288      * @param string $theme themename
1289      * @param string $testtype test type (contexts|features)
1290      * @return array list of blacklisted contexts or features
1291      */
1292     protected function get_blacklisted_tests_for_theme($theme, $testtype) {
1294         $themetestpath = $this->get_theme_test_directory($theme);
1296         if (file_exists($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json')) {
1297             // Blacklist file exist. Leave it for last to clear the feature and contexts.
1298             $blacklisttests = @json_decode(file_get_contents($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json'), true);
1299             if (empty($blacklisttests)) {
1300                 behat_error(BEHAT_EXITCODE_REQUIREMENT, $themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json is empty');
1301             }
1303             // If features or contexts not defined then no problem.
1304             if (!isset($blacklisttests[$testtype])) {
1305                 $blacklisttests[$testtype] = array();
1306             }
1307             return $blacklisttests[$testtype];
1308         }
1310         return array();
1311     }
1313     /**
1314      * Return list of features and step definitions in theme.
1315      *
1316      * @param string $theme theme name
1317      * @param string $testtype test type, either features or contexts
1318      * @return array list of contexts $contexts or $features
1319      */
1320     protected function get_tests_for_theme($theme, $testtype) {
1322         $tests = array();
1323         $testtypes = array(
1324             'contexts' => '|behat_.*\.php$|',
1325             'features' => '|.*\.feature$|',
1326         );
1328         // Get all the directories having overridden tests.
1329         $directories = $this->get_test_directories_overridden_for_theme($theme, $testtype);
1331         // Get overridden test contexts.
1332         foreach ($directories as $dirpath) {
1333             // All behat_*.php inside overridden directory.
1334             $diriterator = new DirectoryIterator($dirpath);
1335             $regite = new RegexIterator($diriterator, $testtypes[$testtype]);
1337             // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files.
1338             foreach ($regite as $file) {
1339                 $key = $file->getBasename('.php');
1340                 $tests[$key] = $file->getPathname();
1341             }
1342         }
1344         return $tests;
1345     }
1347     /**
1348      * Return list of blacklisted behat features for theme and features defined by theme only.
1349      *
1350      * @param string $theme theme name.
1351      * @return array ($blacklistfeatures, $blacklisttags, $features)
1352      */
1353     protected function get_behat_features_for_theme($theme) {
1354         global $CFG;
1356         // Get list of features defined by theme.
1357         $themefeatures = $this->get_tests_for_theme($theme, 'features');
1358         $themeblacklistfeatures = $this->get_blacklisted_tests_for_theme($theme, 'features');
1359         $themeblacklisttags = $this->get_blacklisted_tests_for_theme($theme, 'tags');
1361         // Mobile app tests are not theme-specific, so run only for the default theme (and if
1362         // configured).
1363         if (empty($CFG->behat_ionic_dirroot) || $theme !== $this->get_default_theme()) {
1364             $themeblacklisttags[] = '@app';
1365         }
1367         // Clean feature key and path.
1368         $features = array();
1369         $blacklistfeatures = array();
1371         foreach ($themefeatures as $themefeature) {
1372             list($featurekey, $featurepath) = $this->get_clean_feature_key_and_path($themefeature);
1373             $features[$featurekey] = $featurepath;
1374         }
1376         foreach ($themeblacklistfeatures as $themeblacklistfeature) {
1377             list($blacklistfeaturekey, $blacklistfeaturepath) = $this->get_clean_feature_key_and_path($themeblacklistfeature);
1378             $blacklistfeatures[$blacklistfeaturekey] = $blacklistfeaturepath;
1379         }
1381         // If blacklist tags then add those features to list.
1382         if (!empty($themeblacklisttags)) {
1383             // Remove @ if given, so we are sure we have only tag names.
1384             $themeblacklisttags = array_map(function($v) {
1385                 return ltrim($v, '@');
1386             }, $themeblacklisttags);
1388             $themeblacklisttags = '@' . implode(',@', $themeblacklisttags);
1389             $blacklistedfeatureswithtag = $this->filtered_features_with_tags($this->get_components_features(),
1390                 $themeblacklisttags);
1392             // Add features with blacklisted tags.
1393             if (!empty($blacklistedfeatureswithtag)) {
1394                 foreach ($blacklistedfeatureswithtag as $themeblacklistfeature) {
1395                     list($key, $path) = $this->get_clean_feature_key_and_path($themeblacklistfeature);
1396                     $blacklistfeatures[$key] = $path;
1397                 }
1398             }
1399         }
1401         ksort($features);
1403         $retval = array(
1404             'blacklistfeatures' => $blacklistfeatures,
1405             'features' => $features
1406         );
1408         return $retval;
1409     }
1411     /**
1412      * Return list of contexts overridden by themes.
1413      *
1414      * @return array.
1415      */
1416     protected function get_overridden_theme_contexts() {
1417         if (empty($this->overriddenthemescontexts)) {
1418             $this->overriddenthemescontexts = array();
1419         }
1421         return $this->overriddenthemescontexts;
1422     }
1424     /**
1425      * Return list of behat contexts for theme and update $this->stepdefinitions list.
1426      *
1427      * @param string $theme theme name.
1428      * @return array list($themecontexts, $themesuitecontexts)
1429      */
1430     protected function get_behat_contexts_for_theme($theme) {
1432         // If we already have this list then just return. This will not change by run.
1433         if (!empty($this->themecontexts[$theme]) && !empty($this->themesuitecontexts)) {
1434             return array(
1435                 'contexts' => $this->themecontexts[$theme],
1436                 'suitecontexts' => $this->themesuitecontexts[$theme],
1437             );
1438         }
1440         if (empty($this->overriddenthemescontexts)) {
1441             $this->overriddenthemescontexts = array();
1442         }
1444         $contexts = $this->get_components_contexts();
1446         // Create list of contexts used by theme suite.
1447         $themecontexts = $this->get_tests_for_theme($theme, 'contexts');
1448         $blacklistedcontexts = $this->get_blacklisted_tests_for_theme($theme, 'contexts');
1450         // Theme suite will use all core contexts, except the one overridden by theme.
1451         $themesuitecontexts = $contexts;
1453         foreach ($themecontexts as $context => $path) {
1455             // If a context in theme starts with behat_theme_{themename}_behat_* then it's overriding core context.
1456             if (preg_match('/^behat_theme_'.$theme.'_(\w+)$/', $context, $match)) {
1458                 if (!empty($themesuitecontexts[$match[1]])) {
1459                     unset($themesuitecontexts[$match[1]]);
1460                 }
1462                 // Add this to the list of overridden paths, so it can be added to final contexts list for class resolver.
1463                 $this->overriddenthemescontexts[$context] = $path;
1464             }
1466             $selectortypes = ['named_partial', 'named_exact'];
1467             foreach ($selectortypes as $selectortype) {
1468                 // Don't include selector classes.
1469                 if ($context === self::get_behat_theme_selector_override_classname($theme, $selectortype)) {
1470                     unset($this->contexts[$context]);
1471                     unset($themesuitecontexts[$context]);
1472                     continue;
1473                 }
1474             }
1476             // Add theme specific contexts with suffix to steps definitions.
1477             $themesuitecontexts[$context] = $path;
1478         }
1480         // Remove blacklisted contexts.
1481         foreach ($blacklistedcontexts as $blacklistpath) {
1482             $blacklistcontext = basename($blacklistpath, '.php');
1484             unset($themesuitecontexts[$blacklistcontext]);
1485         }
1487         // We are only interested in the class name of context.
1488         $this->themesuitecontexts[$theme] = $themesuitecontexts;
1489         $this->themecontexts[$theme] = $themecontexts;
1491         $retval = array(
1492             'contexts' => $themecontexts,
1493             'suitecontexts' => $themesuitecontexts,
1494         );
1496         return $retval;
1497     }