Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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_dirroot)) { 760 // Get app version from package.json. 761 $jsonpath = $CFG->behat_ionic_dirroot . '/package.json'; 762 $json = @file_get_contents($jsonpath); 763 if (!$json) { 764 throw new coding_exception('Unable to load app version from ' . $jsonpath); 765 } 766 $package = json_decode($json); 767 if ($package === null || empty($package->version)) { 768 throw new coding_exception('Invalid app package data in ' . $jsonpath); 769 } 770 $installedversion = $package->version; 771 } else if (!empty($CFG->behat_ionic_wwwroot)) { 772 // Get app version from env.json inside wwwroot. 773 $jsonurl = $CFG->behat_ionic_wwwroot . '/assets/env.json'; 774 $json = @file_get_contents($jsonurl); 775 if (!$json) { 776 // Fall back to ionic 3 config file. 777 $jsonurl = $CFG->behat_ionic_wwwroot . '/config.json'; 778 $json = @file_get_contents($jsonurl); 779 if (!$json) { 780 throw new coding_exception('Unable to load app version from ' . $jsonurl); 781 } 782 $config = json_decode($json); 783 if ($config === null || empty($config->versionname)) { 784 throw new coding_exception('Invalid app config data in ' . $jsonurl); 785 } 786 $installedversion = str_replace('-dev', '', $config->versionname); 787 } else { 788 $env = json_decode($json); 789 if (empty($env->build->version ?? null)) { 790 throw new coding_exception('Invalid app config data in ' . $jsonurl); 791 } 792 $installedversion = $env->build->version; 793 } 794 } else { 795 return ''; 796 } 797 798 // Read all feature files to check which mobile tags are used. (Note: This could be cached 799 // but ideally, it is the sort of thing that really ought to be refreshed by doing a new 800 // Behat init. Also, at time of coding it only takes 0.3 seconds and only if app enabled.) 801 $usedtags = []; 802 foreach ($this->features as $filepath) { 803 $feature = file_get_contents($filepath); 804 // This may incorrectly detect versions used e.g. in a comment or something, but it 805 // doesn't do much harm if we have extra ones. 806 if (preg_match_all('~@app_(?:from|upto)(?:[0-9]+(?:\.[0-9]+)*)~', $feature, $matches)) { 807 foreach ($matches[0] as $tag) { 808 // Store as key in array so we don't get duplicates. 809 $usedtags[$tag] = true; 810 } 811 } 812 } 813 814 // Set up relevant tags for each version. 815 $tags = []; 816 foreach ($usedtags as $usedtag => $ignored) { 817 if (!preg_match('~^@app_(from|upto)([0-9]+(?:\.[0-9]+)*)$~', $usedtag, $matches)) { 818 throw new coding_exception('Unexpected tag format'); 819 } 820 $direction = $matches[1]; 821 $version = $matches[2]; 822 823 switch (version_compare($installedversion, $version)) { 824 case -1: 825 // Installed version OLDER than the one being considered, so do not 826 // include any scenarios that only run from the considered version up. 827 if ($direction === 'from') { 828 $tags[] = '~@app_from' . $version; 829 } 830 break; 831 832 case 0: 833 // Installed version EQUAL to the one being considered - no tags need 834 // excluding. 835 break; 836 837 case 1: 838 // Installed version NEWER than the one being considered, so do not 839 // include any scenarios that only run up to that version. 840 if ($direction === 'upto') { 841 $tags[] = '~@app_upto' . $version; 842 } 843 break; 844 } 845 } 846 847 if ($verbose) { 848 mtrace('Configured app tests for version ' . $installedversion); 849 } 850 851 return join(' && ', $tags); 852 } 853 854 /** 855 * Attempt to split feature list into fairish buckets using timing information, if available. 856 * Simply add each one to lightest buckets until all files allocated. 857 * PGA = Profile Guided Allocation. I made it up just now. 858 * CAUTION: workers must agree on allocation, do not be random anywhere! 859 * 860 * @param array $features Behat feature files array 861 * @param int $nbuckets Number of buckets to divide into 862 * @param int $instance Index number of this instance 863 * @return array|bool Feature files array, sorted into allocations 864 */ 865 public function profile_guided_allocate($features, $nbuckets, $instance) { 866 867 // No profile guided allocation is required in phpunit. 868 if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { 869 return false; 870 } 871 872 $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') && 873 @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false; 874 875 if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) { 876 // No data available, fall back to relying on steps data. 877 $stepfile = ""; 878 if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) { 879 $stepfile = BEHAT_FEATURE_STEP_FILE; 880 } 881 // We should never get this. But in case we can't do this then fall back on simple splitting. 882 if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) { 883 return false; 884 } 885 } 886 887 arsort($behattimingdata); // Ensure most expensive is first. 888 889 $realroot = realpath(__DIR__.'/../../../').'/'; 890 $defaultweight = array_sum($behattimingdata) / count($behattimingdata); 891 $weights = array_fill(0, $nbuckets, 0); 892 $buckets = array_fill(0, $nbuckets, array()); 893 $totalweight = 0; 894 895 // Re-key the features list to match timing data. 896 foreach ($features as $k => $file) { 897 $key = str_replace($realroot, '', $file); 898 $features[$key] = $file; 899 unset($features[$k]); 900 if (!isset($behattimingdata[$key])) { 901 $behattimingdata[$key] = $defaultweight; 902 } 903 } 904 905 // Sort features by known weights; largest ones should be allocated first. 906 $behattimingorder = array(); 907 foreach ($features as $key => $file) { 908 $behattimingorder[$key] = $behattimingdata[$key]; 909 } 910 arsort($behattimingorder); 911 912 // Finally, add each feature one by one to the lightest bucket. 913 foreach ($behattimingorder as $key => $weight) { 914 $file = $features[$key]; 915 $lightbucket = array_search(min($weights), $weights); 916 $weights[$lightbucket] += $weight; 917 $buckets[$lightbucket][] = $file; 918 $totalweight += $weight; 919 } 920 921 if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets 922 && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) { 923 echo "Bucket weightings:\n"; 924 foreach ($weights as $k => $weight) { 925 echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL; 926 } 927 } 928 929 // Return the features for this worker. 930 return $buckets[$instance - 1]; 931 } 932 933 /** 934 * Overrides default config with local config values 935 * 936 * array_merge does not merge completely the array's values 937 * 938 * @param mixed $config The node of the default config 939 * @param mixed $localconfig The node of the local config 940 * @return mixed The merge result 941 */ 942 public function merge_config($config, $localconfig) { 943 944 if (!is_array($config) && !is_array($localconfig)) { 945 return $localconfig; 946 } 947 948 // Local overrides also deeper default values. 949 if (is_array($config) && !is_array($localconfig)) { 950 return $localconfig; 951 } 952 953 foreach ($localconfig as $key => $value) { 954 955 // If defaults are not as deep as local values let locals override. 956 if (!is_array($config)) { 957 unset($config); 958 } 959 960 // Add the param if it doesn't exists or merge branches. 961 if (empty($config[$key])) { 962 $config[$key] = $value; 963 } else { 964 $config[$key] = $this->merge_config($config[$key], $localconfig[$key]); 965 } 966 } 967 968 return $config; 969 } 970 971 /** 972 * Merges $CFG->behat_config with the one passed. 973 * 974 * @param array $config existing config. 975 * @return array merged config with $CFG->behat_config 976 */ 977 public function merge_behat_config($config) { 978 global $CFG; 979 980 // In case user defined overrides respect them over our default ones. 981 if (!empty($CFG->behat_config)) { 982 foreach ($CFG->behat_config as $profile => $values) { 983 $values = $this->fix_legacy_profile_data($profile, $values); 984 $config = $this->merge_config($config, $this->get_behat_config_for_profile($profile, $values)); 985 } 986 } 987 988 return $config; 989 } 990 991 /** 992 * Parse $CFG->behat_config and return the array with required config structure for behat.yml 993 * 994 * @param string $profile profile name 995 * @param array $values values for profile 996 * @return array 997 */ 998 public function get_behat_config_for_profile($profile, $values) { 999 // Only add profile which are compatible with Behat 3.x 1000 // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs 1001 // Like : rerun_cache etc. 1002 if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) { 1003 return array($profile => $values); 1004 } 1005 1006 // Parse 2.5 format and get related values. 1007 $oldconfigvalues = array(); 1008 if (isset($values['extensions']['Behat\MinkExtension\Extension'])) { 1009 $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension']; 1010 if (isset($extensionvalues['webdriver']['browser'])) { 1011 $oldconfigvalues['browser'] = $extensionvalues['webdriver']['browser']; 1012 } 1013 if (isset($extensionvalues['webdriver']['wd_host'])) { 1014 $oldconfigvalues['wd_host'] = $extensionvalues['webdriver']['wd_host']; 1015 } 1016 if (isset($extensionvalues['capabilities'])) { 1017 $oldconfigvalues['capabilities'] = $extensionvalues['capabilities']; 1018 } 1019 } 1020 1021 if (isset($values['filters']['tags'])) { 1022 $oldconfigvalues['tags'] = $values['filters']['tags']; 1023 } 1024 1025 if (!empty($oldconfigvalues)) { 1026 behat_config_manager::$autoprofileconversion = true; 1027 return $this->get_behat_profile($profile, $oldconfigvalues); 1028 } 1029 1030 // If nothing set above then return empty array. 1031 return array(); 1032 } 1033 1034 /** 1035 * Merges $CFG->behat_profiles with the one passed. 1036 * 1037 * @param array $config existing config. 1038 * @return array merged config with $CFG->behat_profiles 1039 */ 1040 public function merge_behat_profiles($config) { 1041 global $CFG; 1042 1043 // Check for Moodle custom ones. 1044 if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) { 1045 foreach ($CFG->behat_profiles as $profile => $values) { 1046 $config = $this->merge_config($config, $this->get_behat_profile($profile, $values)); 1047 } 1048 } 1049 1050 return $config; 1051 } 1052 1053 /** 1054 * Check for and attempt to fix legacy profile data. 1055 * 1056 * The Mink Driver used for W3C no longer uses the `selenium2` naming but otherwise is backwards compatibly. 1057 * 1058 * Emit a warning that users should update their configuration. 1059 * 1060 * @param string $profilename The name of this profile 1061 * @param array $data The profile data for this profile 1062 * @return array Th eamended profile data 1063 */ 1064 protected function fix_legacy_profile_data(string $profilename, array $data): array { 1065 // Check for legacy instaclick profiles. 1066 if (!array_key_exists('Behat\MinkExtension', $data['extensions'])) { 1067 return $data; 1068 } 1069 if (array_key_exists('selenium2', $data['extensions']['Behat\MinkExtension'])) { 1070 echo("\n\n"); 1071 echo("=> Warning: Legacy selenium2 profileuration was found for {$profilename} profile.\n"); 1072 echo("=> This has been renamed from 'selenium2' to 'webdriver'.\n"); 1073 echo("=> You should update your Behat configuration.\n"); 1074 echo("\n"); 1075 $data['extensions']['Behat\MinkExtension']['webdriver'] = $data['extensions']['Behat\MinkExtension']['selenium2']; 1076 unset($data['extensions']['Behat\MinkExtension']['selenium2']); 1077 } 1078 1079 return $data; 1080 } 1081 1082 /** 1083 * Cleans the path returned by get_components_with_tests() to standarize it 1084 * 1085 * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/ 1086 * @param string $path 1087 * @return string The string without the last /tests part 1088 */ 1089 public final function clean_path($path) { 1090 1091 $path = rtrim($path, DIRECTORY_SEPARATOR); 1092 1093 $parttoremove = DIRECTORY_SEPARATOR . 'tests'; 1094 1095 $substr = substr($path, strlen($path) - strlen($parttoremove)); 1096 if ($substr == $parttoremove) { 1097 $path = substr($path, 0, strlen($path) - strlen($parttoremove)); 1098 } 1099 1100 return rtrim($path, DIRECTORY_SEPARATOR); 1101 } 1102 1103 /** 1104 * The relative path where components stores their behat tests 1105 * 1106 * @return string 1107 */ 1108 public static final function get_behat_tests_path() { 1109 return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat'; 1110 } 1111 1112 /** 1113 * Return context name of behat_theme selector to use. 1114 * 1115 * @param string $themename name of the theme. 1116 * @param string $selectortype The type of selector (partial or exact at this stage) 1117 * @param bool $includeclass if class should be included. 1118 * @return string 1119 */ 1120 public static final function get_behat_theme_selector_override_classname($themename, $selectortype, $includeclass = false) { 1121 global $CFG; 1122 1123 if ($selectortype !== 'named_partial' && $selectortype !== 'named_exact') { 1124 throw new coding_exception("Unknown selector override type '{$selectortype}'"); 1125 } 1126 1127 $overridebehatclassname = "behat_theme_{$themename}_behat_{$selectortype}_selectors"; 1128 1129 if ($includeclass) { 1130 $themeoverrideselector = $CFG->dirroot . DIRECTORY_SEPARATOR . 'theme' . DIRECTORY_SEPARATOR . $themename . 1131 self::get_behat_tests_path() . DIRECTORY_SEPARATOR . $overridebehatclassname . '.php'; 1132 1133 if (file_exists($themeoverrideselector)) { 1134 require_once($themeoverrideselector); 1135 } 1136 } 1137 1138 return $overridebehatclassname; 1139 } 1140 1141 /** 1142 * List of components which contain behat context or features. 1143 * 1144 * @return array 1145 */ 1146 protected function get_components_with_tests() { 1147 if (empty($this->componentswithtests)) { 1148 $this->componentswithtests = tests_finder::get_components_with_tests('behat'); 1149 } 1150 1151 return $this->componentswithtests; 1152 } 1153 1154 /** 1155 * Remove list of blacklisted features from the feature list. 1156 * 1157 * @param array $features list of original features. 1158 * @param array|string $blacklist list of features which needs to be removed. 1159 * @return array features - blacklisted features. 1160 */ 1161 protected function remove_blacklisted_features_from_list($features, $blacklist) { 1162 1163 // If no blacklist passed then return. 1164 if (empty($blacklist)) { 1165 return $features; 1166 } 1167 1168 // If there is no feature in suite then just return what was passed. 1169 if (empty($features)) { 1170 return $features; 1171 } 1172 1173 if (!is_array($blacklist)) { 1174 $blacklist = array($blacklist); 1175 } 1176 1177 // Remove blacklisted features. 1178 foreach ($blacklist as $blacklistpath) { 1179 1180 list($key, $featurepath) = $this->get_clean_feature_key_and_path($blacklistpath); 1181 1182 if (isset($features[$key])) { 1183 $features[$key] = null; 1184 unset($features[$key]); 1185 } else { 1186 $featurestocheck = $this->get_components_features(); 1187 if (!isset($featurestocheck[$key]) && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) { 1188 behat_error(BEHAT_EXITCODE_REQUIREMENT, 'Blacklisted feature "' . $blacklistpath . '" not found.'); 1189 } 1190 } 1191 } 1192 1193 return $features; 1194 } 1195 1196 /** 1197 * Return list of behat suites. Multiple suites are returned if theme 1198 * overrides default step definitions/features. 1199 * 1200 * @param int $parallelruns number of parallel runs 1201 * @param int $currentrun current run. 1202 * @return array list of suites. 1203 */ 1204 protected function get_behat_suites($parallelruns = 0, $currentrun = 0) { 1205 $features = $this->get_components_features(); 1206 1207 // Get number of parallel runs and current run. 1208 if (!empty($parallelruns) && !empty($currentrun)) { 1209 $this->set_parallel_run($parallelruns, $currentrun); 1210 } else { 1211 $parallelruns = $this->get_number_of_parallel_run(); 1212 $currentrun = $this->get_current_run();; 1213 } 1214 1215 $themefeatures = array(); 1216 $themecontexts = array(); 1217 1218 $themes = $this->get_list_of_themes(); 1219 1220 // Create list of theme suite features and contexts. 1221 foreach ($themes as $theme) { 1222 // Get theme features and contexts. 1223 $themefeatures[$theme] = $this->get_behat_features_for_theme($theme); 1224 $themecontexts[$theme] = $this->get_behat_contexts_for_theme($theme); 1225 } 1226 1227 // Remove list of theme features for default suite, as default suite should not run theme specific features. 1228 foreach ($themefeatures as $themename => $removethemefeatures) { 1229 if (!empty($removethemefeatures['features'])) { 1230 $features = $this->remove_blacklisted_features_from_list($features, $removethemefeatures['features']); 1231 } 1232 } 1233 1234 // Set suite for each theme. 1235 $suites = array(); 1236 foreach ($themes as $theme) { 1237 // Get list of features which will be included in theme. 1238 // If theme suite with all features or default theme, then we want all core features to be part of theme suite. 1239 if ((is_string($this->themesuitewithallfeatures) && ($this->themesuitewithallfeatures === self::ALL_THEMES_TO_RUN)) || 1240 in_array($theme, $this->themesuitewithallfeatures) || ($this->get_default_theme() === $theme)) { 1241 // If there is no theme specific feature. Then it's just core features. 1242 if (empty($themefeatures[$theme]['features'])) { 1243 $themesuitefeatures = $features; 1244 } else { 1245 $themesuitefeatures = array_merge($features, $themefeatures[$theme]['features']); 1246 } 1247 } else { 1248 $themesuitefeatures = $themefeatures[$theme]['features']; 1249 } 1250 1251 // Remove blacklisted features. 1252 $themesuitefeatures = $this->remove_blacklisted_features_from_list($themesuitefeatures, 1253 $themefeatures[$theme]['blacklistfeatures']); 1254 1255 // Return sub-set of features if parallel run. 1256 $themesuitefeatures = $this->get_features_for_the_run($themesuitefeatures, $parallelruns, $currentrun); 1257 1258 // Default theme is part of default suite. 1259 if ($this->get_default_theme() === $theme) { 1260 $suitename = 'default'; 1261 } else { 1262 $suitename = $theme; 1263 } 1264 1265 // Add suite no matter what. If there is no feature in suite then it will just exist successfully with no scenarios. 1266 // But if we don't set this then the user has to know which run doesn't have suite and which run do. 1267 $suites = array_merge($suites, array( 1268 $suitename => array( 1269 'paths' => array_values($themesuitefeatures), 1270 'contexts' => $themecontexts[$theme], 1271 ) 1272 )); 1273 } 1274 1275 return $suites; 1276 } 1277 1278 /** 1279 * Return name of default theme. 1280 * 1281 * @return string 1282 */ 1283 protected function get_default_theme() { 1284 return theme_config::DEFAULT_THEME; 1285 } 1286 1287 /** 1288 * Return list of themes which can be set in moodle. 1289 * 1290 * @return array list of themes with tests. 1291 */ 1292 protected function get_list_of_themes() { 1293 $selectablethemes = array(); 1294 1295 // Get all themes installed on site. 1296 $themes = core_component::get_plugin_list('theme'); 1297 ksort($themes); 1298 1299 foreach ($themes as $themename => $themedir) { 1300 // Load the theme config. 1301 try { 1302 $theme = $this->get_theme_config($themename); 1303 } catch (Exception $e) { 1304 // Bad theme, just skip it for now. 1305 continue; 1306 } 1307 if ($themename !== $theme->name) { 1308 // Obsoleted or broken theme, just skip for now. 1309 continue; 1310 } 1311 if ($theme->hidefromselector) { 1312 // The theme doesn't want to be shown in the theme selector and as theme 1313 // designer mode is switched off we will respect that decision. 1314 continue; 1315 } 1316 $selectablethemes[] = $themename; 1317 } 1318 1319 return $selectablethemes; 1320 } 1321 1322 /** 1323 * Return the theme config for a given theme name. 1324 * This is done so we can mock it in PHPUnit. 1325 * 1326 * @param string $themename name of theme 1327 * @return theme_config 1328 */ 1329 public function get_theme_config($themename) { 1330 return theme_config::load($themename); 1331 } 1332 1333 /** 1334 * Return theme directory. 1335 * 1336 * @param string $themename name of theme 1337 * @return string theme directory 1338 */ 1339 protected function get_theme_test_directory($themename) { 1340 global $CFG; 1341 1342 $themetestdir = "/theme/" . $themename; 1343 1344 return $CFG->dirroot . $themetestdir . self::get_behat_tests_path(); 1345 } 1346 1347 /** 1348 * Returns all the directories having overridden tests. 1349 * 1350 * @param string $theme name of theme 1351 * @param string $testtype The kind of test we are looking for 1352 * @return array all directories having tests 1353 */ 1354 protected function get_test_directories_overridden_for_theme($theme, $testtype) { 1355 global $CFG; 1356 1357 $testtypes = array( 1358 'contexts' => '|behat_.*\.php$|', 1359 'features' => '|.*\.feature$|', 1360 ); 1361 $themetestdirfullpath = $this->get_theme_test_directory($theme); 1362 1363 // If test directory doesn't exist then return. 1364 if (!is_dir($themetestdirfullpath)) { 1365 return array(); 1366 } 1367 1368 $directoriestosearch = glob($themetestdirfullpath . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR); 1369 1370 // Include theme directory to find tests. 1371 $dirs[realpath($themetestdirfullpath)] = trim(str_replace('/', '_', $themetestdirfullpath), '_'); 1372 1373 // Search for tests in valid directories. 1374 foreach ($directoriestosearch as $dir) { 1375 $dirite = new RecursiveDirectoryIterator($dir); 1376 $iteite = new RecursiveIteratorIterator($dirite); 1377 $regexp = $testtypes[$testtype]; 1378 $regite = new RegexIterator($iteite, $regexp); 1379 foreach ($regite as $path => $element) { 1380 $key = dirname($path); 1381 $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_'); 1382 $dirs[$key] = $value; 1383 } 1384 } 1385 ksort($dirs); 1386 1387 return array_flip($dirs); 1388 } 1389 1390 /** 1391 * Return blacklisted contexts or features for a theme, as defined in blacklist.json. 1392 * 1393 * @param string $theme themename 1394 * @param string $testtype test type (contexts|features) 1395 * @return array list of blacklisted contexts or features 1396 */ 1397 protected function get_blacklisted_tests_for_theme($theme, $testtype) { 1398 1399 $themetestpath = $this->get_theme_test_directory($theme); 1400 1401 if (file_exists($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json')) { 1402 // Blacklist file exist. Leave it for last to clear the feature and contexts. 1403 $blacklisttests = @json_decode(file_get_contents($themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json'), true); 1404 if (empty($blacklisttests)) { 1405 behat_error(BEHAT_EXITCODE_REQUIREMENT, $themetestpath . DIRECTORY_SEPARATOR . 'blacklist.json is empty'); 1406 } 1407 1408 // If features or contexts not defined then no problem. 1409 if (!isset($blacklisttests[$testtype])) { 1410 $blacklisttests[$testtype] = array(); 1411 } 1412 return $blacklisttests[$testtype]; 1413 } 1414 1415 return array(); 1416 } 1417 1418 /** 1419 * Return list of features and step definitions in theme. 1420 * 1421 * @param string $theme theme name 1422 * @param string $testtype test type, either features or contexts 1423 * @return array list of contexts $contexts or $features 1424 */ 1425 protected function get_tests_for_theme($theme, $testtype) { 1426 1427 $tests = array(); 1428 $testtypes = array( 1429 'contexts' => '|^behat_.*\.php$|', 1430 'features' => '|.*\.feature$|', 1431 ); 1432 1433 // Get all the directories having overridden tests. 1434 $directories = $this->get_test_directories_overridden_for_theme($theme, $testtype); 1435 1436 // Get overridden test contexts. 1437 foreach ($directories as $dirpath) { 1438 // All behat_*.php inside overridden directory. 1439 $diriterator = new DirectoryIterator($dirpath); 1440 $regite = new RegexIterator($diriterator, $testtypes[$testtype]); 1441 1442 // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files. 1443 foreach ($regite as $file) { 1444 $key = $file->getBasename('.php'); 1445 $tests[$key] = $file->getPathname(); 1446 } 1447 } 1448 1449 return $tests; 1450 } 1451 1452 /** 1453 * Return list of blacklisted behat features for theme and features defined by theme only. 1454 * 1455 * @param string $theme theme name. 1456 * @return array ($blacklistfeatures, $blacklisttags, $features) 1457 */ 1458 protected function get_behat_features_for_theme($theme) { 1459 global $CFG; 1460 1461 // Get list of features defined by theme. 1462 $themefeatures = $this->get_tests_for_theme($theme, 'features'); 1463 $themeblacklistfeatures = $this->get_blacklisted_tests_for_theme($theme, 'features'); 1464 $themeblacklisttags = $this->get_blacklisted_tests_for_theme($theme, 'tags'); 1465 1466 // Mobile app tests are not theme-specific, so run only for the default theme (and if 1467 // configured). 1468 if ((empty($CFG->behat_ionic_dirroot) && empty($CFG->behat_ionic_wwwroot)) || 1469 $theme !== $this->get_default_theme()) { 1470 $themeblacklisttags[] = '@app'; 1471 } 1472 1473 // Clean feature key and path. 1474 $features = array(); 1475 $blacklistfeatures = array(); 1476 1477 foreach ($themefeatures as $themefeature) { 1478 list($featurekey, $featurepath) = $this->get_clean_feature_key_and_path($themefeature); 1479 $features[$featurekey] = $featurepath; 1480 } 1481 1482 foreach ($themeblacklistfeatures as $themeblacklistfeature) { 1483 list($blacklistfeaturekey, $blacklistfeaturepath) = $this->get_clean_feature_key_and_path($themeblacklistfeature); 1484 $blacklistfeatures[$blacklistfeaturekey] = $blacklistfeaturepath; 1485 } 1486 1487 // If blacklist tags then add those features to list. 1488 if (!empty($themeblacklisttags)) { 1489 // Remove @ if given, so we are sure we have only tag names. 1490 $themeblacklisttags = array_map(function($v) { 1491 return ltrim($v, '@'); 1492 }, $themeblacklisttags); 1493 1494 $themeblacklisttags = '@' . implode(',@', $themeblacklisttags); 1495 $blacklistedfeatureswithtag = $this->filtered_features_with_tags($this->get_components_features(), 1496 $themeblacklisttags); 1497 1498 // Add features with blacklisted tags. 1499 if (!empty($blacklistedfeatureswithtag)) { 1500 foreach ($blacklistedfeatureswithtag as $themeblacklistfeature) { 1501 list($key, $path) = $this->get_clean_feature_key_and_path($themeblacklistfeature); 1502 $blacklistfeatures[$key] = $path; 1503 } 1504 } 1505 } 1506 1507 ksort($features); 1508 1509 $retval = array( 1510 'blacklistfeatures' => $blacklistfeatures, 1511 'features' => $features 1512 ); 1513 1514 return $retval; 1515 } 1516 1517 /** 1518 * Return list of behat contexts for theme and update $this->stepdefinitions list. 1519 * 1520 * @param string $theme theme name. 1521 * @return List of contexts 1522 */ 1523 protected function get_behat_contexts_for_theme($theme) : array { 1524 // If we already have this list then just return. This will not change by run. 1525 if (!empty($this->themecontexts[$theme])) { 1526 return $this->themecontexts[$theme]; 1527 } 1528 1529 try { 1530 $themeconfig = $this->get_theme_config($theme); 1531 } catch (Exception $e) { 1532 // This theme has no theme config. 1533 return []; 1534 } 1535 1536 // The theme will use all core contexts, except the one overridden by theme or its parent. 1537 $parentcontexts = []; 1538 if (isset($themeconfig->parents)) { 1539 foreach ($themeconfig->parents as $parent) { 1540 if ($parentcontexts = $this->get_behat_contexts_for_theme($parent)) { 1541 break; 1542 } 1543 } 1544 } 1545 1546 if (empty($parentcontexts)) { 1547 $parentcontexts = $this->get_components_contexts(); 1548 } 1549 1550 // Remove contexts which have been actively blacklisted. 1551 $blacklistedcontexts = $this->get_blacklisted_tests_for_theme($theme, 'contexts'); 1552 foreach ($blacklistedcontexts as $blacklistpath) { 1553 $blacklistcontext = basename($blacklistpath, '.php'); 1554 1555 unset($parentcontexts[$blacklistcontext]); 1556 } 1557 1558 // Apply overrides. 1559 $contexts = array_merge($parentcontexts, $this->get_tests_for_theme($theme, 'contexts')); 1560 1561 // Remove classes which are overridden. 1562 foreach ($contexts as $contextclass => $path) { 1563 require_once($path); 1564 if (!class_exists($contextclass)) { 1565 // This may be a Poorly named class. 1566 continue; 1567 } 1568 1569 $rc = new \ReflectionClass($contextclass); 1570 while ($rc = $rc->getParentClass()) { 1571 if (isset($contexts[$rc->name])) { 1572 unset($contexts[$rc->name]); 1573 } 1574 } 1575 } 1576 1577 // Sort the list of contexts. 1578 $contexts = $this->sort_component_contexts($contexts); 1579 1580 // Cache it for subsequent fetches. 1581 $this->themecontexts[$theme] = $contexts; 1582 1583 return $contexts; 1584 } 1585 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body