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