From e99f0cbba6ccd725e6885b51641780267958a75e Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Thu, 28 Jul 2016 11:53:38 +0800 Subject: [PATCH] MDL-55072 behat: refactor behat_config_manager --- lib/behat/classes/behat_config_manager.php | 367 ++----------- lib/behat/classes/behat_config_util.php | 599 +++++++++++++++++++++ lib/testing/classes/tests_finder.php | 3 + lib/upgrade.txt | 10 + 4 files changed, 667 insertions(+), 312 deletions(-) create mode 100644 lib/behat/classes/behat_config_util.php diff --git a/lib/behat/classes/behat_config_manager.php b/lib/behat/classes/behat_config_manager.php index 54049f5aeb2..b3af2b5c4e5 100644 --- a/lib/behat/classes/behat_config_manager.php +++ b/lib/behat/classes/behat_config_manager.php @@ -25,9 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -require_once(__DIR__ . '/../lib.php'); -require_once(__DIR__ . '/behat_command.php'); -require_once(__DIR__ . '/../../testing/classes/tests_finder.php'); +require_once(__DIR__ . '/behat_config_util.php'); /** * Behat configuration manager @@ -47,6 +45,24 @@ class behat_config_manager { */ public static $autoprofileconversion = false; + /** + * @var behat_config_util keep object of behat_config_util for use. + */ + public static $behatconfigutil = null; + + /** + * Returns behat_config_util. + * + * @return behat_config_util + */ + private static function get_behat_config_util() { + if (!self::$behatconfigutil) { + self::$behatconfigutil = new behat_config_util(); + } + + return self::$behatconfigutil; + } + /** * Updates a config file * @@ -72,41 +88,15 @@ class behat_config_manager { $configfilepath = self::get_steps_list_config_filepath(); } - // Gets all the components with features. + // Gets all the components with features, if running the tests otherwise not required. $features = array(); - $components = tests_finder::get_components_with_tests('features'); - if ($components) { - foreach ($components as $componentname => $path) { - $path = self::clean_path($path) . self::get_behat_tests_path(); - if (empty($featurespaths[$path]) && file_exists($path)) { - - // Standarizes separator (some dirs. comes with OS-dependant separator). - $uniquekey = str_replace('\\', '/', $path); - $featurespaths[$uniquekey] = $path; - } - } - foreach ($featurespaths as $path) { - $additional = glob("$path/*.feature"); - $features = array_merge($features, $additional); - } - } - - // Optionally include features from additional directories. - if (!empty($CFG->behat_additionalfeatures)) { - $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures)); + if ($testsrunner) { + $features = self::get_behat_config_util()->get_components_features(); + $features = self::get_behat_config_util()->get_features_with_tags($features, $tags); } // Gets all the components with steps definitions. - $stepsdefinitions = array(); - $steps = self::get_components_steps_definitions(); - if ($steps) { - foreach ($steps as $key => $filepath) { - if ($component == '' || $component === $key) { - $stepsdefinitions[$key] = $filepath; - } - } - } - + $stepsdefinitions = self::get_behat_config_util()->get_components_steps_definitions($component); // We don't want the deprecated steps definitions here. if (!$testsrunner) { unset($stepsdefinitions['behat_deprecated']); @@ -114,7 +104,7 @@ class behat_config_manager { // Behat config file specifing the main context class, // the required Behat extensions and Moodle test wwwroot. - $contents = self::get_config_file_contents(self::get_features_with_tags($features, $tags), $stepsdefinitions); + $contents = self::get_behat_config_util()->get_config_file_contents($features, $stepsdefinitions); // Stores the file. if (!file_put_contents($configfilepath, $contents)) { @@ -129,55 +119,13 @@ class behat_config_manager { * @param array $features set of feature files. * @param string $tags list of tags (currently support && only.) * @return array filtered list of feature files with tags. + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ public static function get_features_with_tags($features, $tags) { - if (empty($tags)) { - return $features; - } - $newfeaturelist = array(); - // Split tags in and and or. - $tags = explode('&&', $tags); - $andtags = array(); - $ortags = array(); - foreach ($tags as $tag) { - // Explode all tags seperated by , and add it to ortags. - $ortags = array_merge($ortags, explode(',', $tag)); - // And tags will be the first one before comma(,). - $andtags[] = preg_replace('/,.*/', '', $tag); - } - foreach ($features as $featurefile) { - $contents = file_get_contents($featurefile); - $includefeature = true; - foreach ($andtags as $tag) { - // If negitive tag, then ensure it don't exist. - if (strpos($tag, '~') !== false) { - $tag = substr($tag, 1); - if ($contents && strpos($contents, $tag) !== false) { - $includefeature = false; - break; - } - } else if ($contents && strpos($contents, $tag) === false) { - $includefeature = false; - break; - } - } - - // If feature not included then check or tags. - if (!$includefeature && !empty($ortags)) { - foreach ($ortags as $tag) { - if ($contents && (strpos($tag, '~') === false) && (strpos($contents, $tag) !== false)) { - $includefeature = true; - break; - } - } - } - - if ($includefeature) { - $newfeaturelist[] = $featurefile; - } - } - return $newfeaturelist; + debugging('Use of get_features_with_tags is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->get_features_with_tags($features, $tags); } /** @@ -189,32 +137,13 @@ class behat_config_manager { * it from the steps definitions web interface * * @return array + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ public static function get_components_steps_definitions() { - $components = tests_finder::get_components_with_tests('stepsdefinitions'); - if (!$components) { - return false; - } - - $stepsdefinitions = array(); - foreach ($components as $componentname => $componentpath) { - $componentpath = self::clean_path($componentpath); - - if (!file_exists($componentpath . self::get_behat_tests_path())) { - continue; - } - $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path()); - $regite = new RegexIterator($diriterator, '|behat_.*\.php$|'); - - // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files. - foreach ($regite as $file) { - $key = $file->getBasename('.php'); - $stepsdefinitions[$key] = $file->getPathname(); - } - } - - return $stepsdefinitions; + debugging('Use of get_components_steps_definitions is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->get_components_steps_definitions(); } /** @@ -361,91 +290,13 @@ class behat_config_manager { * @param array $features The system feature files * @param array $stepsdefinitions The system steps definitions * @return string + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ protected static function get_config_file_contents($features, $stepsdefinitions) { - global $CFG; - // We require here when we are sure behat dependencies are available. - require_once($CFG->dirroot . '/vendor/autoload.php'); - - $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub'); - - $parallelruns = self::get_parallel_test_runs(); - // If parallel run, then only divide features. - if (!empty($CFG->behatrunprocess) && !empty($parallelruns)) { - // Attempt to split into weighted buckets using timing information, if available. - if ($alloc = self::profile_guided_allocate($features, max(1, $parallelruns), $CFG->behatrunprocess)) { - $features = $alloc; - } else { - // Divide the list of feature files amongst the parallel runners. - srand(crc32(floor(time() / 3600 / 24) . var_export($features, true))); - shuffle($features); - // Pull out the features for just this worker. - if (count($features)) { - $features = array_chunk($features, ceil(count($features) / max(1, $parallelruns))); - // Check if there is any feature file for this process. - if (!empty($features[$CFG->behatrunprocess - 1])) { - $features = $features[$CFG->behatrunprocess - 1]; - } else { - $features = null; - } - } - } - // Set proper selenium2 wd_host if defined. - if (!empty($CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host'])) { - $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host']); - } - } - - // It is possible that it has no value as we don't require a full behat setup to list the step definitions. - if (empty($CFG->behat_wwwroot)) { - $CFG->behat_wwwroot = 'http://itwillnotbeused.com'; - } - - // Comments use black color, so failure path is not visible. Using color other then black/white is safer. - // https://github.com/Behat/Behat/pull/628. - $config = array( - 'default' => array( - 'formatters' => array( - 'moodle_progress' => array( - 'output_styles' => array( - 'comment' => array('magenta')) - ) - ), - 'suites' => array( - 'default' => array( - 'paths' => $features, - 'contexts' => array_keys($stepsdefinitions) - ) - ), - 'extensions' => array( - 'Behat\MinkExtension' => array( - 'base_url' => $CFG->behat_wwwroot, - 'goutte' => null, - 'selenium2' => $selenium2wdhost - ), - 'Moodle\BehatExtension' => array( - 'moodledirroot' => $CFG->dirroot, - 'steps_definitions' => $stepsdefinitions - ) - ) - ) - ); - - // In case user defined overrides respect them over our default ones. - if (!empty($CFG->behat_config)) { - foreach ($CFG->behat_config as $profile => $values) { - $config = self::merge_config($config, self::merge_behat_config($profile, $values)); - } - } - // Check for Moodle custom ones. - if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) { - foreach ($CFG->behat_profiles as $profile => $values) { - $config = self::merge_config($config, self::get_behat_profile($profile, $values)); - } - } - - return Symfony\Component\Yaml\Yaml::dump($config, 10, 2); + debugging('Use of get_config_file_contents is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->get_config_file_contents($features, $stepsdefinitions); } /** @@ -454,41 +305,13 @@ class behat_config_manager { * @param string $profile profile name * @param array $values values for profile * @return array + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ protected static function merge_behat_config($profile, $values) { - // Only add profile which are compatible with Behat 3.x - // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs - // Like : rerun_cache etc. - if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) { - return array($profile => $values); - } - // Parse 2.5 format and get related values. - $oldconfigvalues = array(); - if (isset($values['extensions']['Behat\MinkExtension\Extension'])) { - $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension']; - if (isset($extensionvalues['selenium2']['browser'])) { - $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser']; - } - if (isset($extensionvalues['selenium2']['wd_host'])) { - $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host']; - } - if (isset($extensionvalues['capabilities'])) { - $oldconfigvalues['capabilities'] = $extensionvalues['capabilities']; - } - } - - if (isset($values['filters']['tags'])) { - $oldconfigvalues['tags'] = $values['filters']['tags']; - } - - if (!empty($oldconfigvalues)) { - self::$autoprofileconversion = true; - return self::get_behat_profile($profile, $oldconfigvalues); - } - - // If nothing set above then return empty array. - return array(); + debugging('Use of merge_behat_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + self::get_behat_config_util()->get_behat_config_for_profile($profile, $values); } /** @@ -569,64 +392,8 @@ class behat_config_manager { */ protected static function profile_guided_allocate($features, $nbuckets, $instance) { - $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') && - @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false; - - if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) { - // No data available, fall back to relying on steps data. - $stepfile = ""; - if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) { - $stepfile = BEHAT_FEATURE_STEP_FILE; - } - // We should never get this. But in case we can't do this then fall back on simple splitting. - if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) { - return false; - } - } - - arsort($behattimingdata); // Ensure most expensive is first. - - $realroot = realpath(__DIR__.'/../../../').'/'; - $defaultweight = array_sum($behattimingdata) / count($behattimingdata); - $weights = array_fill(0, $nbuckets, 0); - $buckets = array_fill(0, $nbuckets, array()); - $totalweight = 0; - - // Re-key the features list to match timing data. - foreach ($features as $k => $file) { - $key = str_replace($realroot, '', $file); - $features[$key] = $file; - unset($features[$k]); - if (!isset($behattimingdata[$key])) { - $behattimingdata[$key] = $defaultweight; - } - } - - // Sort features by known weights; largest ones should be allocated first. - $behattimingorder = array(); - foreach ($features as $key => $file) { - $behattimingorder[$key] = $behattimingdata[$key]; - } - arsort($behattimingorder); - - // Finally, add each feature one by one to the lightest bucket. - foreach ($behattimingorder as $key => $weight) { - $file = $features[$key]; - $lightbucket = array_search(min($weights), $weights); - $weights[$lightbucket] += $weight; - $buckets[$lightbucket][] = $file; - $totalweight += $weight; - } - - if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets) { - echo "Bucket weightings:\n"; - foreach ($weights as $k => $weight) { - echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL; - } - } - - // Return the features for this worker. - return $buckets[$instance - 1]; + debugging('Use of profile_guided_allocate is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->profile_guided_allocate($features, $nbuckets, $instance); } /** @@ -637,34 +404,13 @@ class behat_config_manager { * @param mixed $config The node of the default config * @param mixed $localconfig The node of the local config * @return mixed The merge result + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ protected static function merge_config($config, $localconfig) { - if (!is_array($config) && !is_array($localconfig)) { - return $localconfig; - } - - // Local overrides also deeper default values. - if (is_array($config) && !is_array($localconfig)) { - return $localconfig; - } - - foreach ($localconfig as $key => $value) { - - // If defaults are not as deep as local values let locals override. - if (!is_array($config)) { - unset($config); - } - - // Add the param if it doesn't exists or merge branches. - if (empty($config[$key])) { - $config[$key] = $value; - } else { - $config[$key] = self::merge_config($config[$key], $localconfig[$key]); - } - } - - return $config; + debugging('Use of merge_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->merge_config($config, $localconfig); } /** @@ -673,28 +419,25 @@ class behat_config_manager { * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/ * @param string $path * @return string The string without the last /tests part + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ protected final static function clean_path($path) { - $path = rtrim($path, DIRECTORY_SEPARATOR); - - $parttoremove = DIRECTORY_SEPARATOR . 'tests'; - - $substr = substr($path, strlen($path) - strlen($parttoremove)); - if ($substr == $parttoremove) { - $path = substr($path, 0, strlen($path) - strlen($parttoremove)); - } - - return rtrim($path, DIRECTORY_SEPARATOR); + debugging('Use of clean_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->clean_path($path); } /** * The relative path where components stores their behat tests * * @return string + * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php + * @todo MDL-55365 This will be deleted in Moodle 3.6. */ protected final static function get_behat_tests_path() { - return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat'; + debugging('Use of get_behat_tests_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER); + return self::get_behat_config_util()->get_behat_tests_path(); } } diff --git a/lib/behat/classes/behat_config_util.php b/lib/behat/classes/behat_config_util.php new file mode 100644 index 00000000000..4f685a14c8f --- /dev/null +++ b/lib/behat/classes/behat_config_util.php @@ -0,0 +1,599 @@ +. + +/** + * Utils to set Behat config + * + * @package core_behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../lib.php'); +require_once(__DIR__ . '/behat_command.php'); +require_once(__DIR__ . '/../../testing/classes/tests_finder.php'); + +/** + * Behat configuration manager + * + * Creates/updates Behat config files getting tests + * and steps from Moodle codebase + * + * @package core_behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_config_util { + + /** + * @var array list of features in core. + */ + private $features; + + /** + * @var array list of stepsdefinitions containing in core. + */ + private $stepsdefinitions; + + /** + * @var array list of components with tests. + */ + private $componentswithtests; + + /** + * @var bool Keep track of the automatic profile conversion. So we can notify user. + */ + public static $autoprofileconversion = false; + + /** + * List of components which contain behat context or features. + * + * @return array + */ + private function get_components_with_tests() { + if (empty($this->componentswithtests)) { + $this->componentswithtests = tests_finder::get_components_with_tests('behat'); + } + + return $this->componentswithtests; + } + + /** + * Return list of features. + * + * @return array + */ + public function get_components_features() { + global $CFG; + + // If we already have a list created then just return that, as it's up-to-date. + if (!empty($this->features)) { + return $this->features; + } + + // Gets all the components with features. + $this->features = array(); + $featurespaths = array(); + $components = $this->get_components_with_tests(); + + if ($components) { + foreach ($components as $componentname => $path) { + $path = $this->clean_path($path) . $this->get_behat_tests_path(); + if (empty($featurespaths[$path]) && file_exists($path)) { + + // Standarizes separator (some dirs. comes with OS-dependant separator). + $uniquekey = str_replace('\\', '/', $path); + $featurespaths[$uniquekey] = $path; + } + } + foreach ($featurespaths as $path) { + $additional = glob("$path/*.feature"); + $this->features = array_merge($this->features, $additional); + } + } + + // Optionally include features from additional directories. + if (!empty($CFG->behat_additionalfeatures)) { + $this->features = array_merge($this->features, array_map("realpath", $CFG->behat_additionalfeatures)); + } + + return $this->features; + } + + /** + * Gets the list of Moodle steps definitions + * + * Class name as a key and the filepath as value + * + * Externalized from update_config_file() to use + * it from the steps definitions web interface + * + * @param string $component Restricts the obtained steps definitions to the specified component + * @return array + */ + public function get_components_steps_definitions($component = '') { + + // If we already have a list created then just return that, as it's up-to-date. + if (!empty($this->stepsdefinitions)) { + return $this->stepsdefinitions; + } + + $components = $this->get_components_with_tests(); + + $this->stepsdefinitions = array(); + foreach ($components as $componentname => $componentpath) { + $componentpath = self::clean_path($componentpath); + + if (!file_exists($componentpath . self::get_behat_tests_path())) { + continue; + } + $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path()); + $regite = new RegexIterator($diriterator, '|behat_.*\.php$|'); + + // All behat_*.php inside behat_config_manager::get_behat_tests_path() are added as steps definitions files. + foreach ($regite as $file) { + $key = $file->getBasename('.php'); + if ($component == '' || $component === $key) { + $this->stepsdefinitions[$key] = $file->getPathname(); + } + } + } + + return $this->stepsdefinitions; + } + + /** + * Search feature files for set of tags. + * + * @param array $features set of feature files. + * @param string $tags list of tags (currently support && only.) + * @return array filtered list of feature files with tags. + */ + public function get_features_with_tags($features, $tags) { + if (empty($tags)) { + return $features; + } + $newfeaturelist = array(); + // Split tags in and and or. + $tags = explode('&&', $tags); + $andtags = array(); + $ortags = array(); + foreach ($tags as $tag) { + // Explode all tags seperated by , and add it to ortags. + $ortags = array_merge($ortags, explode(',', $tag)); + // And tags will be the first one before comma(,). + $andtags[] = preg_replace('/,.*/', '', $tag); + } + + foreach ($features as $featurefile) { + $contents = file_get_contents($featurefile); + $includefeature = true; + foreach ($andtags as $tag) { + // If negitive tag, then ensure it don't exist. + if (strpos($tag, '~') !== false) { + $tag = substr($tag, 1); + if ($contents && strpos($contents, $tag) !== false) { + $includefeature = false; + break; + } + } else if ($contents && strpos($contents, $tag) === false) { + $includefeature = false; + break; + } + } + + // If feature not included then check or tags. + if (!$includefeature && !empty($ortags)) { + foreach ($ortags as $tag) { + if ($contents && (strpos($tag, '~') === false) && (strpos($contents, $tag) !== false)) { + $includefeature = true; + break; + } + } + } + + if ($includefeature) { + $newfeaturelist[] = $featurefile; + } + } + return $newfeaturelist; + } + + /** + * Behat config file specifing the main context class, + * the required Behat extensions and Moodle test wwwroot. + * + * @param array $features The system feature files + * @param array $stepsdefinitions The system steps definitions + * @return string + */ + public function get_config_file_contents($features = '', $stepsdefinitions = '', $tags = '') { + global $CFG; + + // If features not passed then get it. + if (empty($features)) { + $features = $this->get_components_features(); + $features = $this->get_features_with_tags($features, $tags); + } + + // If stepdefinitions not passed then get the list. + if (empty($stepsdefinitions)) { + $this->get_components_steps_definitions(); + } + + // We require here when we are sure behat dependencies are available. + require_once($CFG->dirroot . '/vendor/autoload.php'); + + $selenium2wdhost = array('wd_host' => 'http://localhost:4444/wd/hub'); + + $parallelruns = behat_config_manager::get_parallel_test_runs(); + // If parallel run, then only divide features. + if (!empty($CFG->behatrunprocess) && !empty($parallelruns)) { + // Attempt to split into weighted buckets using timing information, if available. + if ($alloc = $this->profile_guided_allocate($features, max(1, $parallelruns), $CFG->behatrunprocess)) { + $features = $alloc; + } else { + // Divide the list of feature files amongst the parallel runners. + srand(crc32(floor(time() / 3600 / 24) . var_export($features, true))); + shuffle($features); + // Pull out the features for just this worker. + if (count($features)) { + $features = array_chunk($features, ceil(count($features) / max(1, $parallelruns))); + // Check if there is any feature file for this process. + if (!empty($features[$CFG->behatrunprocess - 1])) { + $features = $features[$CFG->behatrunprocess - 1]; + } else { + $features = null; + } + } + } + // Set proper selenium2 wd_host if defined. + if (!empty($CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host'])) { + $selenium2wdhost = array('wd_host' => $CFG->behat_parallel_run[$CFG->behatrunprocess - 1]['wd_host']); + } + } + + // It is possible that it has no value as we don't require a full behat setup to list the step definitions. + if (empty($CFG->behat_wwwroot)) { + $CFG->behat_wwwroot = 'http://itwillnotbeused.com'; + } + + // Comments use black color, so failure path is not visible. Using color other then black/white is safer. + // https://github.com/Behat/Behat/pull/628. + $config = array( + 'default' => array( + 'formatters' => array( + 'moodle_progress' => array( + 'output_styles' => array( + 'comment' => array('magenta')) + ) + ), + 'suites' => array( + 'default' => array( + 'paths' => $features, + 'contexts' => array_keys($stepsdefinitions) + ) + ), + 'extensions' => array( + 'Behat\MinkExtension' => array( + 'base_url' => $CFG->behat_wwwroot, + 'goutte' => null, + 'selenium2' => $selenium2wdhost + ), + 'Moodle\BehatExtension' => array( + 'moodledirroot' => $CFG->dirroot, + 'steps_definitions' => $stepsdefinitions + ) + ) + ) + ); + + $config = $this->merge_behat_config($config); + + $config = $this->merge_behat_profiles($config); + + return Symfony\Component\Yaml\Yaml::dump($config, 10, 2); + } + + /** + * Parse $CFG->behat_profile and return the array with required config structure for behat.yml. + * + * $CFG->behat_profiles = array( + * 'profile' = array( + * 'browser' => 'firefox', + * 'tags' => '@javascript', + * 'wd_host' => 'http://127.0.0.1:4444/wd/hub', + * 'capabilities' => array( + * 'platform' => 'Linux', + * 'version' => 44 + * ) + * ) + * ); + * + * @param string $profile profile name + * @param array $values values for profile. + * @return array + */ + protected function get_behat_profile($profile, $values) { + // Values should be an array. + if (!is_array($values)) { + return array(); + } + + // Check suite values. + $behatprofilesuites = array(); + // Fill tags information. + if (isset($values['tags'])) { + $behatprofilesuites = array( + 'suites' => array( + 'default' => array( + 'filters' => array( + 'tags' => $values['tags'], + ) + ) + ) + ); + } + + // Selenium2 config values. + $behatprofileextension = array(); + $seleniumconfig = array(); + if (isset($values['browser'])) { + $seleniumconfig['browser'] = $values['browser']; + } + if (isset($values['wd_host'])) { + $seleniumconfig['wd_host'] = $values['wd_host']; + } + if (isset($values['capabilities'])) { + $seleniumconfig['capabilities'] = $values['capabilities']; + } + if (!empty($seleniumconfig)) { + $behatprofileextension = array( + 'extensions' => array( + 'Behat\MinkExtension' => array( + 'selenium2' => $seleniumconfig, + ) + ) + ); + } + + return array($profile => array_merge($behatprofilesuites, $behatprofileextension)); + } + + /** + * Attempt to split feature list into fairish buckets using timing information, if available. + * Simply add each one to lightest buckets until all files allocated. + * PGA = Profile Guided Allocation. I made it up just now. + * CAUTION: workers must agree on allocation, do not be random anywhere! + * + * @param array $features Behat feature files array + * @param int $nbuckets Number of buckets to divide into + * @param int $instance Index number of this instance + * @return array|bool Feature files array, sorted into allocations + */ + public function profile_guided_allocate($features, $nbuckets, $instance) { + + $behattimingfile = defined('BEHAT_FEATURE_TIMING_FILE') && + @filesize(BEHAT_FEATURE_TIMING_FILE) ? BEHAT_FEATURE_TIMING_FILE : false; + + if (!$behattimingfile || !$behattimingdata = @json_decode(file_get_contents($behattimingfile), true)) { + // No data available, fall back to relying on steps data. + $stepfile = ""; + if (defined('BEHAT_FEATURE_STEP_FILE') && BEHAT_FEATURE_STEP_FILE) { + $stepfile = BEHAT_FEATURE_STEP_FILE; + } + // We should never get this. But in case we can't do this then fall back on simple splitting. + if (empty($stepfile) || !$behattimingdata = @json_decode(file_get_contents($stepfile), true)) { + return false; + } + } + + arsort($behattimingdata); // Ensure most expensive is first. + + $realroot = realpath(__DIR__.'/../../../').'/'; + $defaultweight = array_sum($behattimingdata) / count($behattimingdata); + $weights = array_fill(0, $nbuckets, 0); + $buckets = array_fill(0, $nbuckets, array()); + $totalweight = 0; + + // Re-key the features list to match timing data. + foreach ($features as $k => $file) { + $key = str_replace($realroot, '', $file); + $features[$key] = $file; + unset($features[$k]); + if (!isset($behattimingdata[$key])) { + $behattimingdata[$key] = $defaultweight; + } + } + + // Sort features by known weights; largest ones should be allocated first. + $behattimingorder = array(); + foreach ($features as $key => $file) { + $behattimingorder[$key] = $behattimingdata[$key]; + } + arsort($behattimingorder); + + // Finally, add each feature one by one to the lightest bucket. + foreach ($behattimingorder as $key => $weight) { + $file = $features[$key]; + $lightbucket = array_search(min($weights), $weights); + $weights[$lightbucket] += $weight; + $buckets[$lightbucket][] = $file; + $totalweight += $weight; + } + + if ($totalweight && !defined('BEHAT_DISABLE_HISTOGRAM') && $instance == $nbuckets) { + echo "Bucket weightings:\n"; + foreach ($weights as $k => $weight) { + echo $k + 1 . ": " . str_repeat('*', 70 * $nbuckets * $weight / $totalweight) . PHP_EOL; + } + } + + // Return the features for this worker. + return $buckets[$instance - 1]; + } + + /** + * Overrides default config with local config values + * + * array_merge does not merge completely the array's values + * + * @param mixed $config The node of the default config + * @param mixed $localconfig The node of the local config + * @return mixed The merge result + */ + public function merge_config($config, $localconfig) { + + if (!is_array($config) && !is_array($localconfig)) { + return $localconfig; + } + + // Local overrides also deeper default values. + if (is_array($config) && !is_array($localconfig)) { + return $localconfig; + } + + foreach ($localconfig as $key => $value) { + + // If defaults are not as deep as local values let locals override. + if (!is_array($config)) { + unset($config); + } + + // Add the param if it doesn't exists or merge branches. + if (empty($config[$key])) { + $config[$key] = $value; + } else { + $config[$key] = $this->merge_config($config[$key], $localconfig[$key]); + } + } + + return $config; + } + + /** + * Merges $CFG->behat_config with the one passed. + * + * @param array $config existing config. + * @return array merged config with $CFG->behat_config + */ + public function merge_behat_config($config) { + global $CFG; + + // In case user defined overrides respect them over our default ones. + if (!empty($CFG->behat_config)) { + foreach ($CFG->behat_config as $profile => $values) { + $config = $this->merge_config($config, $this->get_behat_config_for_profile($profile, $values)); + } + } + + return $config; + } + + /** + * Parse $CFG->behat_config and return the array with required config structure for behat.yml + * + * @param string $profile profile name + * @param array $values values for profile + * @return array + */ + public function get_behat_config_for_profile($profile, $values) { + // Only add profile which are compatible with Behat 3.x + // Just check if any of Bheat 2.5 config is set. Not checking for 3.x as it might have some other configs + // Like : rerun_cache etc. + if (!isset($values['filters']['tags']) && !isset($values['extensions']['Behat\MinkExtension\Extension'])) { + return array($profile => $values); + } + + // Parse 2.5 format and get related values. + $oldconfigvalues = array(); + if (isset($values['extensions']['Behat\MinkExtension\Extension'])) { + $extensionvalues = $values['extensions']['Behat\MinkExtension\Extension']; + if (isset($extensionvalues['selenium2']['browser'])) { + $oldconfigvalues['browser'] = $extensionvalues['selenium2']['browser']; + } + if (isset($extensionvalues['selenium2']['wd_host'])) { + $oldconfigvalues['wd_host'] = $extensionvalues['selenium2']['wd_host']; + } + if (isset($extensionvalues['capabilities'])) { + $oldconfigvalues['capabilities'] = $extensionvalues['capabilities']; + } + } + + if (isset($values['filters']['tags'])) { + $oldconfigvalues['tags'] = $values['filters']['tags']; + } + + if (!empty($oldconfigvalues)) { + behat_config_manager::$autoprofileconversion = true; + return $this->get_behat_profile($profile, $oldconfigvalues); + } + + // If nothing set above then return empty array. + return array(); + } + + /** + * Merges $CFG->behat_profiles with the one passed. + * + * @param array $config existing config. + * @return array merged config with $CFG->behat_profiles + */ + public function merge_behat_profiles($config) { + global $CFG; + + // Check for Moodle custom ones. + if (!empty($CFG->behat_profiles) && is_array($CFG->behat_profiles)) { + foreach ($CFG->behat_profiles as $profile => $values) { + $config = $this->merge_config($config, $this->get_behat_profile($profile, $values)); + } + } + + return $config; + } + + /** + * Cleans the path returned by get_components_with_tests() to standarize it + * + * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/ + * @param string $path + * @return string The string without the last /tests part + */ + public final function clean_path($path) { + + $path = rtrim($path, DIRECTORY_SEPARATOR); + + $parttoremove = DIRECTORY_SEPARATOR . 'tests'; + + $substr = substr($path, strlen($path) - strlen($parttoremove)); + if ($substr == $parttoremove) { + $path = substr($path, 0, strlen($path) - strlen($parttoremove)); + } + + return rtrim($path, DIRECTORY_SEPARATOR); + } + + /** + * The relative path where components stores their behat tests + * + * @return string + */ + public final static function get_behat_tests_path() { + return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat'; + } +} \ No newline at end of file diff --git a/lib/testing/classes/tests_finder.php b/lib/testing/classes/tests_finder.php index 946ec231346..b93ef233fd0 100644 --- a/lib/testing/classes/tests_finder.php +++ b/lib/testing/classes/tests_finder.php @@ -197,6 +197,9 @@ class tests_finder { case 'stepsdefinitions': $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|'; break; + case 'behat': + $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!'; + break; } return $regexp; diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 10a5207d648..4435c2ddb0d 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -55,6 +55,16 @@ information provided here is intended especially for developers. * New functions to support deprecation of events have been added to the base event. See MDL-46214 for further details. * A new function `get_name_with_info` has been added to the base event. This function adds information about event deprecations and should be used where this information is relevant. +* Following api's have been deprecated in behat_config_manager, please use behat_config_util instead. + - get_features_with_tags + - get_components_steps_definitions + - get_config_file_contents + - merge_behat_config + - get_behat_profile + - profile_guided_allocate + - merge_config + - clean_path + - get_behat_tests_path === 3.1 ===