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