[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 * @category test 22 * @copyright 2012 David Monllaó 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once (__DIR__ . '/../lib.php'); 29 require_once (__DIR__ . '/behat_command.php'); 30 require_once (__DIR__ . '/../../testing/classes/tests_finder.php'); 31 32 /** 33 * Behat configuration manager 34 * 35 * Creates/updates Behat config files getting tests 36 * and steps from Moodle codebase 37 * 38 * @package core 39 * @category test 40 * @copyright 2012 David Monllaó 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 class behat_config_manager { 44 45 /** 46 * @var bool Keep track of the automatic profile conversion. So we can notify user. 47 */ 48 public static $autoprofileconversion = false; 49 50 /** 51 * Updates a config file 52 * 53 * The tests runner and the steps definitions list uses different 54 * config files to avoid problems with concurrent executions. 55 * 56 * The steps definitions list can be filtered by component so it's 57 * behat.yml is different from the $CFG->dirroot one. 58 * 59 * @param string $component Restricts the obtained steps definitions to the specified component 60 * @param string $testsrunner If the config file will be used to run tests 61 * @param string $tags features files including tags. 62 * @return void 63 */ 64 public static function update_config_file($component = '', $testsrunner = true, $tags = '') { 65 global $CFG; 66 67 // Behat must have a separate behat.yml to have access to the whole set of features and steps definitions. 68 if ($testsrunner === true) { 69 $configfilepath = behat_command::get_behat_dir() . '/behat.yml'; 70 } else { 71 // Alternative for steps definitions filtering, one for each user. 72 $configfilepath = self::get_steps_list_config_filepath(); 73 } 74 75 // Gets all the components with features. 76 $features = array(); 77 $components = tests_finder::get_components_with_tests('features'); 78 if ($components) { 79 foreach ($components as $componentname => $path) { 80 $path = self::clean_path($path) . self::get_behat_tests_path(); 81 if (empty($featurespaths[$path]) && file_exists($path)) { 82 83 // Standarizes separator (some dirs. comes with OS-dependant separator). 84 $uniquekey = str_replace('\\', '/', $path); 85 $featurespaths[$uniquekey] = $path; 86 } 87 } 88 foreach ($featurespaths as $path) { 89 $additional = glob("$path/*.feature"); 90 $features = array_merge($features, $additional); 91 } 92 } 93 94 // Optionally include features from additional directories. 95 if (!empty($CFG->behat_additionalfeatures)) { 96 $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures)); 97 } 98 99 // Gets all the components with steps definitions. 100 $stepsdefinitions = array(); 101 $steps = self::get_components_steps_definitions(); 102 if ($steps) { 103 foreach ($steps as $key => $filepath) { 104 if ($component == '' || $component === $key) { 105 $stepsdefinitions[$key] = $filepath; 106 } 107 } 108 } 109 110 // We don't want the deprecated steps definitions here. 111 if (!$testsrunner) { 112 unset($stepsdefinitions['behat_deprecated']); 113 } 114 115 // Behat config file specifing the main context class, 116 // the required Behat extensions and Moodle test wwwroot. 117 $contents = self::get_config_file_contents(self::get_features_with_tags($features, $tags), $stepsdefinitions); 118 119 // Stores the file. 120 if (!file_put_contents($configfilepath, $contents)) { 121 behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $configfilepath . ' can not be created'); 122 } 123 124 } 125 126 /** 127 * Search feature files for set of tags. 128 * 129 * @param array $features set of feature files. 130 * @param string $tags list of tags (currently support && only.) 131 * @return array filtered list of feature files with tags. 132 */ 133 public static function get_features_with_tags($features, $tags) { 134 if (empty($tags)) { 135 return $features; 136 } 137 $newfeaturelist = array(); 138 // Split tags in and and or. 139 $tags = explode('&&', $tags); 140 $andtags = array(); 141 $ortags = array(); 142 foreach ($tags as $tag) { 143 // Explode all tags seperated by , and add it to ortags. 144 $ortags = array_merge($ortags, explode(',', $tag)); 145 // And tags will be the first one before comma(,). 146 $andtags[] = preg_replace('/,.*/', '', $tag); 147 } 148 149 foreach ($features as $featurefile) { 150 $contents = file_get_contents($featurefile); 151 $includefeature = true; 152 foreach ($andtags as $tag) { 153 // If negitive tag, then ensure it don't exist. 154 if (strpos($tag, '~') !== false) { 155 $tag = substr($tag, 1); 156 if ($contents && strpos($contents, $tag) !== false) { 157 $includefeature = false; 158 break; 159 } 160 } else if ($contents && strpos($contents, $tag) === false) { 161 $includefeature = false; 162 break; 163 } 164 } 165 166 // If feature not included then check or tags. 167 if (!$includefeature && !empty($ortags)) { 168 foreach ($ortags as $tag) { 169 if ($contents && (strpos($tag, '~') === false) && (strpos($contents, $tag) !== false)) { 170 $includefeature = true; 171 break; 172 } 173 } 174 } 175 176 if ($includefeature) { 177 $newfeaturelist[] = $featurefile; 178 } 179 } 180 return $newfeaturelist; 181 } 182 183 /** 184 * Gets the list of Moodle steps definitions 185 * 186 * Class name as a key and the filepath as value 187 * 188 * Externalized from update_config_file() to use 189 * it from the steps definitions web interface 190 * 191 * @return array 192 */ 193 public static function get_components_steps_definitions() { 194 195 $components = tests_finder::get_components_with_tests('stepsdefinitions'); 196 if (!$components) { 197 return false; 198 } 199 200 $stepsdefinitions = array(); 201 foreach ($components as $componentname => $componentpath) { 202 $componentpath = self::clean_path($componentpath); 203 204 if (!file_exists($componentpath . self::get_behat_tests_path())) { 205 continue; 206 } 207 $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path()); 208 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|'); 209 210 // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files. 211 foreach ($regite as $file) { 212 $key = $file->getBasename('.php'); 213 $stepsdefinitions[$key] = $file->getPathname(); 214 } 215 } 216 217 return $stepsdefinitions; 218 } 219 220 /** 221 * Returns the behat config file path used by the steps definition list 222 * 223 * @return string 224 */ 225 public static function get_steps_list_config_filepath() { 226 global $USER; 227 228 // We don't cygwin-it as it is called using exec() which uses cmd.exe. 229 $userdir = behat_command::get_behat_dir() . '/users/' . $USER->id; 230 make_writable_directory($userdir); 231 232 return $userdir . '/behat.yml'; 233 } 234 235 /** 236 * Returns the behat config file path used by the behat cli command. 237 * 238 * @param int $runprocess Runprocess. 239 * @return string 240 */ 241 public static function get_behat_cli_config_filepath($runprocess = 0) { 242 global $CFG; 243 244 if ($runprocess) { 245 if (isset($CFG->behat_parallel_run[$runprocess - 1 ]['behat_dataroot'])) { 246 $command = $CFG->behat_parallel_run[$runprocess - 1]['behat_dataroot']; 247 } else { 248 $command = $CFG->behat_dataroot . $runprocess; 249 } 250 } else { 251 $command = $CFG->behat_dataroot; 252 } 253 $command .= DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . 'behat.yml'; 254 255 // Cygwin uses linux-style directory separators. 256 if (testing_is_cygwin()) { 257 $command = str_replace('\\', '/', $command); 258 } 259 260 return $command; 261 } 262 263 /** 264 * Returns the path to the parallel run file which specifies if parallel test environment is enabled 265 * and how many parallel runs to execute. 266 * 267 * @param int $runprocess run process for which behat dir is returned. 268 * @return string 269 */ 270 public final static function get_parallel_test_file_path($runprocess = 0) { 271 return behat_command::get_behat_dir($runprocess) . '/parallel_environment_enabled.txt'; 272 } 273 274 /** 275 * Returns number of parallel runs for which site is initialised. 276 * 277 * @param int $runprocess run process for which behat dir is returned. 278 * @return int 279 */ 280 public final static function get_parallel_test_runs($runprocess = 0) { 281 282 $parallelrun = 0; 283 // Get parallel run info from first file and last file. 284 $parallelrunconfigfile = self::get_parallel_test_file_path($runprocess); 285 if (file_exists($parallelrunconfigfile)) { 286 if ($parallel = file_get_contents($parallelrunconfigfile)) { 287 $parallelrun = (int) $parallel; 288 } 289 } 290 291 return $parallelrun; 292 } 293 294 /** 295 * Drops parallel site links. 296 * 297 * @return bool true on success else false. 298 */ 299 public final static function drop_parallel_site_links() { 300 global $CFG; 301 302 // Get parallel test runs from first run. 303 $parallelrun = self::get_parallel_test_runs(1); 304 305 if (empty($parallelrun)) { 306 return false; 307 } 308 309 // If parallel run then remove links and original file. 310 clearstatcache(); 311 for ($i = 1; $i <= $parallelrun; $i++) { 312 // Don't delete links for specified sites, as they should be accessible. 313 if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) { 314 continue; 315 } 316 $link = $CFG->dirroot . '/' . BEHAT_PARALLEL_SITE_NAME . $i; 317 if (file_exists($link) && is_link($link)) { 318 @unlink($link); 319 } 320 } 321 return true; 322 } 323 324 /** 325 * Create parallel site links. 326 * 327 * @param int $fromrun first run 328 * @param int $torun last run. 329 * @return bool true for sucess, else false. 330 */ 331 public final static function create_parallel_site_links($fromrun, $torun) { 332 global $CFG; 333 334 // Create site symlink if necessary. 335 clearstatcache(); 336 for ($i = $fromrun; $i <= $torun; $i++) { 337 // Don't create links for specified sites, as they should be accessible. 338 if (!empty($CFG->behat_parallel_run['behat_wwwroot'][$i - 1]['behat_wwwroot'])) { 339 continue; 340 } 341 $link = $CFG->dirroot.'/'.BEHAT_PARALLEL_SITE_NAME.$i; 342 clearstatcache(); 343 if (file_exists($link)) { 344 if (!is_link($link) || !is_dir($link)) { 345 echo "File exists at link location ($link) but is not a link or directory!" . PHP_EOL; 346 return false; 347 } 348 } else if (!symlink($CFG->dirroot, $link)) { 349 // Try create link in case it's not already present. 350 echo "Unable to create behat site symlink ($link)" . PHP_EOL; 351 return false; 352 } 353 } 354 return true; 355 } 356 357 /** 358 * Behat config file specifing the main context class, 359 * the required Behat extensions and Moodle test wwwroot. 360 * 361 * @param array $features The system feature files 362 * @param array $stepsdefinitions The system steps definitions 363 * @return string 364 */ 365 protected static function get_config_file_contents($features, $stepsdefinitions) { 366 global $CFG; 367 368 // We require here when we are sure behat dependencies are available. 369 require_once($CFG->dirroot . '/vendor/autoload.php'); 370 371 $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub'); 372 373 $parallelruns = self::get_parallel_test_runs(); 374 // If parallel run, then only divide features. 375 if (!empty($CFG->behatrunprocess) && !empty($parallelruns)) { 376 // Attempt to split into weighted buckets using timing information, if available. 377 if ($alloc = self::profile_guided_allocate($features, max(1, $parallelruns), $CFG->behatrunprocess)) { 378 $features = $alloc; 379 } else { 380 // Divide the list of feature files amongst the parallel runners. 381 srand(crc32(floor(time() / 3600 / 24) . var_export($features, true))); 382 shuffle($features); 383 // Pull out the features for just this worker. 384 if (count($features)) { 385 $features = array_chunk($features, ceil(count($features) / max(1, $parallelruns))); 386 // Check if there is any feature file for this process. 387 if (!empty($features[$CFG->behatrunprocess - 1])) { 388 $features = $features[$CFG->behatrunprocess - 1]; 389 } else { 390 $features = null; 391 } 392 } 393 } 394 // Set proper selenium2 wd_host if defined. 395 if (!empty($CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host'])) { 396 $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host']); 397 } 398 } 399 400 // It is possible that it has no value as we don't require a full behat setup to list the step definitions. 401 if (empty($CFG->behat_wwwroot)) { 402 $CFG->behat_wwwroot = 'http://itwillnotbeused.com'; 403 } 404 405 // Comments use black color, so failure path is not visible. Using color other then black/white is safer. 406 // https://github.com/Behat/Behat/pull/628. 407 $config = array( 408 'default' => array( 409 'formatters' => array( 410 'moodle_progress' => array( 411 'output_styles' => array( 412 'comment' => array('magenta')) 413 ) 414 ), 415 'suites' => array( 416 'default' => array( 417 'paths' => $features, 418 'contexts' => array_keys($stepsdefinitions) 419 ) 420 ), 421 'extensions' => array( 422 'Behat\MinkExtension' => array( 423 'base_url' => $CFG->behat_wwwroot, 424 'goutte' => null, 425 'selenium2' => $selenium2wdhost 426 ), 427 'Moodle\BehatExtension' => array( 428 'moodledirroot' => $CFG->dirroot, 429 'steps_definitions' => $stepsdefinitions 430 ) 431 ) 432 ) 433 ); 434 435 // In case user defined overrides respect them over our default ones. 436 if (!empty($CFG->behat_config)) { 437 foreach ($CFG->behat_config as $profile => $values) { 438 $config = self::merge_config($config, self::merge_behat_config($profile, $values)); 439 } 440 } 441 // Check for Moodle custom ones. 442 if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) { 443 foreach ($CFG->behat_profiles as $profile => $values) { 444 $config = self::merge_config($config, self::get_behat_profile($profile, $values)); 445 } 446 } 447 448 return Symfony\Component\Yaml\Yaml::dump($config, 10, 2); 449 } 450 451 /** 452 * Parse $CFG->behat_config and return the array with required config structure for behat.yml 453 * 454 * @param string $profile profile name 455 * @param array $values values for profile 456 * @return array 457 */ 458 protected static function merge_behat_config($profile, $values) { 459 // Only add profile which are compatible with Behat 3.x 460 // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs 461 // Like : rerun_cache etc. 462 if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) { 463 return array($profile => $values); 464 } 465 466 // Parse 2.5 format and get related values. 467 $oldconfigvalues = array(); 468 if (isset($values['extensions']['Behat\MinkExtension\Extension'])) { 469 $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension']; 470 if (isset($extensionvalues['selenium2']['browser'])) { 471 $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser']; 472 } 473 if (isset($extensionvalues['selenium2']['wd_host'])) { 474 $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host']; 475 } 476 if (isset($extensionvalues['capabilities'])) { 477 $oldconfigvalues['capabilities'] = $extensionvalues['capabilities']; 478 } 479 } 480 481 if (isset($values['filters']['tags'])) { 482 $oldconfigvalues['tags'] = $values['filters']['tags']; 483 } 484 485 if (!empty($oldconfigvalues)) { 486 self::$autoprofileconversion = true; 487 return self::get_behat_profile($profile, $oldconfigvalues); 488 } 489 490 // If nothing set above then return empty array. 491 return array(); 492 } 493 494 /** 495 * Parse $CFG->behat_profile and return the array with required config structure for behat.yml. 496 * 497 * $CFG->behat_profiles = array( 498 * 'profile' = array( 499 * 'browser' => 'firefox', 500 * 'tags' => '@javascript', 501 * 'wd_host' => 'http://127.0.0.1:4444/wd/hub', 502 * 'capabilities' => array( 503 * 'platform' => 'Linux', 504 * 'version' => 44 505 * ) 506 * ) 507 * ); 508 * 509 * @param string $profile profile name 510 * @param array $values values for profile. 511 * @return array 512 */ 513 protected static function get_behat_profile($profile, $values) { 514 // Values should be an array. 515 if (!is_array($values)) { 516 return array(); 517 } 518 519 // Check suite values. 520 $behatprofilesuites = array(); 521 // Fill tags information. 522 if (isset($values['tags'])) { 523 $behatprofilesuites = array( 524 'suites' => array( 525 'default' => array( 526 'filters' => array( 527 'tags' => $values['tags'], 528 ) 529 ) 530 ) 531 ); 532 } 533 534 // Selenium2 config values. 535 $behatprofileextension = array(); 536 $seleniumconfig = array(); 537 if (isset($values['browser'])) { 538 $seleniumconfig['browser'] = $values['browser']; 539 } 540 if (isset($values['wd_host'])) { 541 $seleniumconfig['wd_host'] = $values['wd_host']; 542 } 543 if (isset($values['capabilities'])) { 544 $seleniumconfig['capabilities'] = $values['capabilities']; 545 } 546 if (!empty($seleniumconfig)) { 547 $behatprofileextension = array( 548 'extensions' => array( 549 'Behat\MinkExtension' => array( 550 'selenium2' => $seleniumconfig, 551 ) 552 ) 553 ); 554 } 555 556 return array($profile => array_merge($behatprofilesuites, $behatprofileextension)); 557 } 558 559 /** 560 * Attempt to split feature list into fairish buckets using timing information, if available. 561 * Simply add each one to lightest buckets until all files allocated. 562 * PGA = Profile Guided Allocation. I made it up just now. 563 * CAUTION: workers must agree on allocation, do not be random anywhere! 564 * 565 * @param array $features Behat feature files array 566 * @param int $nbuckets Number of buckets to divide into 567 * @param int $instance Index number of this instance 568 * @return array Feature files array, sorted into allocations 569 */ 570 protected static function profile_guided_allocate($features, $nbuckets, $instance) { 571 572 $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') && 573 @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false; 574 575 if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) { 576 // No data available, fall back to relying on steps data. 577 $stepfile = ""; 578 if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) { 579 $stepfile = BEHAT_FEATURE_STEP_FILE; 580 } 581 // We should never get this. But in case we can't do this then fall back on simple splitting. 582 if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) { 583 return false; 584 } 585 } 586 587 arsort($behattimingdata); // Ensure most expensive is first. 588 589 $realroot = realpath(__DIR__.'/../../../').'/'; 590 $defaultweight = array_sum($behattimingdata) / count($behattimingdata); 591 $weights = array_fill(0, $nbuckets, 0); 592 $buckets = array_fill(0, $nbuckets, array()); 593 $totalweight = 0; 594 595 // Re-key the features list to match timing data. 596 foreach ($features as $k => $file) { 597 $key = str_replace($realroot, '', $file); 598 $features[$key] = $file; 599 unset($features[$k]); 600 if (!isset($behattimingdata[$key])) { 601 $behattimingdata[$key] = $defaultweight; 602 } 603 } 604 605 // Sort features by known weights; largest ones should be allocated first. 606 $behattimingorder = array(); 607 foreach ($features as $key => $file) { 608 $behattimingorder[$key] = $behattimingdata[$key]; 609 } 610 arsort($behattimingorder); 611 612 // Finally, add each feature one by one to the lightest bucket. 613 foreach ($behattimingorder as $key => $weight) { 614 $file = $features[$key]; 615 $lightbucket = array_search(min($weights), $weights); 616 $weights[$lightbucket] += $weight; 617 $buckets[$lightbucket][] = $file; 618 $totalweight += $weight; 619 } 620 621 if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets) { 622 echo "Bucket weightings:\n"; 623 foreach ($weights as $k => $weight) { 624 echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL; 625 } 626 } 627 628 // Return the features for this worker. 629 return $buckets[$instance - 1]; 630 } 631 632 /** 633 * Overrides default config with local config values 634 * 635 * array_merge does not merge completely the array's values 636 * 637 * @param mixed $config The node of the default config 638 * @param mixed $localconfig The node of the local config 639 * @return mixed The merge result 640 */ 641 protected static function merge_config($config, $localconfig) { 642 643 if (!is_array($config) && !is_array($localconfig)) { 644 return $localconfig; 645 } 646 647 // Local overrides also deeper default values. 648 if (is_array($config) && !is_array($localconfig)) { 649 return $localconfig; 650 } 651 652 foreach ($localconfig as $key => $value) { 653 654 // If defaults are not as deep as local values let locals override. 655 if (!is_array($config)) { 656 unset($config); 657 } 658 659 // Add the param if it doesn't exists or merge branches. 660 if (empty($config[$key])) { 661 $config[$key] = $value; 662 } else { 663 $config[$key] = self::merge_config($config[$key], $localconfig[$key]); 664 } 665 } 666 667 return $config; 668 } 669 670 /** 671 * Cleans the path returned by get_components_with_tests() to standarize it 672 * 673 * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/ 674 * @param string $path 675 * @return string The string without the last /tests part 676 */ 677 protected final static function clean_path($path) { 678 679 $path = rtrim($path, DIRECTORY_SEPARATOR); 680 681 $parttoremove = DIRECTORY_SEPARATOR . 'tests'; 682 683 $substr = substr($path, strlen($path) - strlen($parttoremove)); 684 if ($substr == $parttoremove) { 685 $path = substr($path, 0, strlen($path) - strlen($parttoremove)); 686 } 687 688 return rtrim($path, DIRECTORY_SEPARATOR); 689 } 690 691 /** 692 * The relative path where components stores their behat tests 693 * 694 * @return string 695 */ 696 protected final static function get_behat_tests_path() { 697 return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat'; 698 } 699 700 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |