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