MDL-37046 behat: Added to standard plugins list
[moodle.git] / admin / tool / behat / locallib.php
CommitLineData
7f541ea3
DM
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 * Behat commands
19 *
20 * @package tool_behat
21 * @copyright 2012 David MonllaĆ³
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
d6abac76 25global $CFG;
7f541ea3
DM
26require_once($CFG->libdir . '/filestorage/file_exceptions.php');
27require_once($CFG->libdir . '/phpunit/bootstraplib.php');
33005f68 28require_once($CFG->libdir . '/phpunit/classes/tests_finder.php');
7f541ea3 29
d46340eb
DM
30require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/behat/steps_definitions_form.php');
31
6d994c51
DM
32/**
33 * Behat commands manager
34 *
6d994c51
DM
35 * @package tool_behat
36 * @copyright 2012 David MonllaĆ³
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 */
7f541ea3
DM
39class tool_behat {
40
0b5f481b 41 /** @var array Steps types */
d46340eb 42 private static $steps_types = array('given', 'when', 'then');
7f541ea3 43
a09534f4 44 /** @var string Docu url */
d46340eb 45 public static $docsurl = 'http://docs.moodle.org/dev/Acceptance_testing';
7f541ea3 46
7f541ea3
DM
47 /**
48 * Lists the available steps definitions
d46340eb
DM
49 *
50 * @param string $type
51 * @param string $component
52 * @param string $filter
53 * @return string
7f541ea3 54 */
d46340eb 55 public static function stepsdefinitions($type, $component, $filter) {
7f541ea3
DM
56 global $CFG;
57
33005f68 58 self::check_behat_setup();
7f541ea3 59
d46340eb 60 // The loaded steps depends on the component specified.
0b5f481b 61 self::update_config_file($component, false);
df8dcf3e 62
a09534f4 63 // The Moodle\BehatExtension\HelpPrinter\MoodleDefinitionsPrinter will parse this search format.
d46340eb
DM
64 if ($type) {
65 $filter .= '&&' . $type;
6d994c51
DM
66 }
67
68 if ($filter) {
d46340eb 69 $filteroption = ' -d "' . $filter . '"';
7f541ea3
DM
70 } else {
71 $filteroption = ' -di';
72 }
73
74 $currentcwd = getcwd();
0b5f481b 75 chdir($CFG->dirroot);
ff2ec29b 76 exec(self::get_behat_command() . ' --config="'.self::get_steps_list_config_filepath(). '" '.$filteroption, $steps, $code);
7f541ea3
DM
77 chdir($currentcwd);
78
7f541ea3 79 if ($steps) {
d46340eb 80 $stepshtml = implode('', $steps);
7f541ea3
DM
81 }
82
d46340eb
DM
83 if (!isset($stepshtml) || $stepshtml == '') {
84 $stepshtml = get_string('nostepsdefinitions', 'tool_behat');
7f541ea3
DM
85 }
86
d46340eb 87 return $stepshtml;
7f541ea3
DM
88 }
89
cc544646 90 /**
0b5f481b 91 * Allows / disables the test environment to be accessed through the built-in server
cca2c806
DM
92 *
93 * Built-in server must be started separately
94 *
6d994c51 95 * @param string $testenvironment enable|disable
cc544646 96 */
0b5f481b 97 public static function switchenvironment($testenvironment) {
cc544646 98 if ($testenvironment == 'enable') {
cca2c806 99 self::start_test_mode();
cc544646 100 } else if ($testenvironment == 'disable') {
cca2c806 101 self::stop_test_mode();
cc544646 102 }
cc544646 103 }
7f541ea3 104
7f541ea3 105 /**
0b5f481b 106 * Updates a config file
cca2c806 107 *
0b5f481b
DM
108 * The tests runner and the steps definitions list uses different
109 * config files to avoid problems with concurrent executions.
110 *
111 * The steps definitions list can be filtered by component so it's
112 * behat.yml can be different from the dirroot one.
cca2c806 113 *
d46340eb 114 * @param string $component Restricts the obtained steps definitions to the specified component
0b5f481b 115 * @param string $testsrunner If the config file will be used to run tests
8cdc0ce8
DM
116 * @throws file_exception
117 */
d6abac76 118 protected static function update_config_file($component = '', $testsrunner = true) {
df8dcf3e 119 global $CFG;
8cdc0ce8 120
0b5f481b
DM
121 // Behat must run with the whole set of features and steps definitions.
122 if ($testsrunner === true) {
123 $prefix = '';
124 $configfilepath = $CFG->dirroot . '/behat.yml';
125
ff2ec29b 126 // Alternative for steps definitions filtering.
0b5f481b
DM
127 } else {
128 $configfilepath = self::get_steps_list_config_filepath();
129 $prefix = $CFG->dirroot .'/';
130 }
df8dcf3e 131
93245e95 132 // Gets all the components with features.
d6abac76 133 $features = array();
8cdc0ce8
DM
134 $components = tests_finder::get_components_with_tests('features');
135 if ($components) {
8cdc0ce8 136 foreach ($components as $componentname => $path) {
0922ef87 137 $path = self::clean_path($path) . self::get_behat_tests_path();
603c95dc 138 if (empty($featurespaths[$path]) && file_exists($path)) {
0922ef87
DM
139 $uniquekey = str_replace('\\', '/', $path);
140 $featurespaths[$uniquekey] = $path;
603c95dc 141 }
8cdc0ce8 142 }
d6abac76 143 $features = array_values($featurespaths);
8cdc0ce8
DM
144 }
145
93245e95 146 // Gets all the components with steps definitions.
d6abac76 147 $stepsdefinitions = array();
d46340eb 148 $steps = self::get_components_steps_definitions();
6d9427bb 149 if ($steps) {
d46340eb
DM
150 foreach ($steps as $key => $filepath) {
151 if ($component == '' || $component === $key) {
d6abac76 152 $stepsdefinitions[$key] = $filepath;
df8dcf3e 153 }
93245e95
DM
154 }
155 }
8cdc0ce8 156
d6abac76
DM
157 // Behat config file specifing the main context class,
158 // the required Behat extensions and Moodle test wwwroot.
159 $contents = self::get_config_file_contents($prefix, $features, $stepsdefinitions);
160
93245e95 161 // Stores the file.
0b5f481b
DM
162 if (!file_put_contents($configfilepath, $contents)) {
163 throw new file_exception('cannotcreatefile', $configfilepath);
8cdc0ce8 164 }
603c95dc 165
8cdc0ce8 166 }
7f541ea3 167
d6abac76
DM
168 /**
169 * Behat config file specifing the main context class,
170 * the required Behat extensions and Moodle test wwwroot.
171 *
172 * @param string $prefix The filesystem prefix
173 * @param array $features The system feature files
174 * @param array $stepsdefinitions The system steps definitions
175 * @return string
176 */
177 protected static function get_config_file_contents($prefix, $features, $stepsdefinitions) {
178 global $CFG;
179
180 // We require here when we are sure behat dependencies are available.
181 require_once($CFG->dirroot . '/vendor/autoload.php');
182
183 $config = array(
184 'default' => array(
185 'paths' => array(
186 'features' => $prefix . 'lib/behat/features',
187 'bootstrap' => $prefix . 'lib/behat/features/bootstrap',
188 ),
189 'context' => array(
190 'class' => 'behat_init_context'
191 ),
192 'extensions' => array(
193 'Behat\MinkExtension\Extension' => array(
bd770617 194 'base_url' => $CFG->behat_wwwroot,
d6abac76
DM
195 'goutte' => null,
196 'selenium2' => null
197 ),
198 'Moodle\BehatExtension\Extension' => array(
199 'features' => $features,
200 'steps_definitions' => $stepsdefinitions
201 )
202 )
203 )
204 );
205
206 // In case user defined overrides respect them over our default ones.
bd770617
DM
207 if (!empty($CFG->behat_config)) {
208 $config = self::merge_config($config, $CFG->behat_config);
d6abac76
DM
209 }
210
211 return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
212 }
213
214 /**
215 * Overrides default config with local config values
216 *
217 * array_merge does not merge completely the array's values
218 *
219 * @param mixed $config The node of the default config
220 * @param mixed $localconfig The node of the local config
221 * @return mixed The merge result
222 */
223 protected static function merge_config($config, $localconfig) {
224
225 if (!is_array($config) && !is_array($localconfig)) {
226 return $localconfig;
227 }
228
229 // Local overrides also deeper default values.
230 if (is_array($config) && !is_array($localconfig)) {
231 return $localconfig;
232 }
233
234 foreach ($localconfig as $key => $value) {
235
236 // If defaults are not as deep as local values let locals override.
237 if (!is_array($config)) {
238 unset($config);
239 }
240
ff2ec29b 241 // Add the param if it doesn't exists or merge branches.
d6abac76
DM
242 if (empty($config[$key])) {
243 $config[$key] = $value;
d6abac76
DM
244 } else {
245 $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
246 }
247 }
248
249 return $config;
250 }
251
d46340eb
DM
252 /**
253 * Gets the list of Moodle steps definitions
254 *
255 * Class name as a key and the filepath as value
256 *
257 * Externalized from update_config_file() to use
258 * it from the steps definitions web interface
259 *
260 * @return array
261 */
262 public static function get_components_steps_definitions() {
263
264 $components = tests_finder::get_components_with_tests('stepsdefinitions');
265 if (!$components) {
266 return false;
267 }
268
269 $stepsdefinitions = array();
270 foreach ($components as $componentname => $componentpath) {
271 $componentpath = self::clean_path($componentpath);
0922ef87 272 $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
d46340eb
DM
273 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
274
0922ef87 275 // All behat_*.php inside self::get_behat_tests_path() are added as steps definitions files.
d46340eb
DM
276 foreach ($regite as $file) {
277 $key = $file->getBasename('.php');
278 $stepsdefinitions[$key] = $file->getPathname();
279 }
280 }
281
282 return $stepsdefinitions;
283 }
284
0b5f481b 285 /**
bd770617 286 * Checks if $CFG->behat_wwwroot is available
0b5f481b
DM
287 *
288 * @return boolean
289 */
290 public static function is_server_running() {
291 global $CFG;
292
293 $request = new curl();
bd770617 294 $request->get($CFG->behat_wwwroot);
0b5f481b
DM
295 return (true && !$request->get_errno());
296 }
d46340eb 297
93245e95
DM
298 /**
299 * Cleans the path returned by get_components_with_tests() to standarize it
300 *
301 * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
302 * @param string $path
303 * @return string The string without the last /tests part
304 */
d6abac76 305 protected static function clean_path($path) {
93245e95 306
0922ef87 307 $path = rtrim($path, DIRECTORY_SEPARATOR);
93245e95 308
0922ef87 309 $parttoremove = DIRECTORY_SEPARATOR . 'tests';
93245e95
DM
310
311 $substr = substr($path, strlen($path) - strlen($parttoremove));
312 if ($substr == $parttoremove) {
313 $path = substr($path, 0, strlen($path) - strlen($parttoremove));
314 }
315
0922ef87 316 return rtrim($path, DIRECTORY_SEPARATOR);
93245e95
DM
317 }
318
7f541ea3
DM
319 /**
320 * Checks whether the test database and dataroot is ready
321 * Stops execution if something went wrong
322 */
d6abac76 323 protected static function test_environment_problem() {
7f541ea3
DM
324 global $CFG;
325
a09534f4
DM
326 // PHPUnit --diag returns nothing if the test environment is set up correctly.
327 exec('php ' . $CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli/util.php --diag', $output, $code);
7f541ea3
DM
328
329 // If something is not ready stop execution and display the CLI command output.
330 if ($code != 0) {
a09534f4 331 notice(get_string('phpunitenvproblem', 'tool_behat') . ': ' . implode(' ', $output));
7f541ea3
DM
332 }
333 }
334
335 /**
cca2c806
DM
336 * Checks if behat is set up and working
337 *
cb18fd71
DM
338 * It checks behat dependencies have been installed and runs
339 * the behat help command to ensure it works as expected
a2a98438 340 * @param boolean $checkphp Extra check for the PHP version
7f541ea3 341 */
d6abac76 342 protected static function check_behat_setup($checkphp = false) {
7f541ea3
DM
343 global $CFG;
344
bd770617
DM
345 // We don't check the PHP version if $CFG->behat_switchcompletely has been enabled.
346 if (empty($CFG->behat_switchcompletely) && $checkphp && version_compare(PHP_VERSION, '5.4.0', '<')) {
a2a98438
DM
347 throw new Exception(get_string('wrongphpversion', 'tool_behat'));
348 }
349
7f541ea3 350 // Moodle setting.
ff2ec29b 351 if (!self::are_behat_dependencies_installed()) {
cb18fd71
DM
352
353 $msg = get_string('wrongbehatsetup', 'tool_behat');
7f541ea3 354
d46340eb 355 // With HTML.
ff2ec29b 356 $docslink = self::$docsurl . '#Installation';
7f541ea3 357 if (!CLI_SCRIPT) {
d46340eb 358 $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
7f541ea3 359 }
d46340eb 360 $msg .= '. ' . get_string('moreinfoin', 'tool_behat') . ' ' . $docslink;
7f541ea3
DM
361 notice($msg);
362 }
363
364 // Behat test command.
365 $currentcwd = getcwd();
0b5f481b
DM
366 chdir($CFG->dirroot);
367 exec(self::get_behat_command() . ' --help', $output, $code);
7f541ea3
DM
368 chdir($currentcwd);
369
370 if ($code != 0) {
371 notice(get_string('wrongbehatsetup', 'tool_behat'));
372 }
373 }
374
375 /**
cca2c806 376 * Enables test mode
7f541ea3 377 *
0b5f481b
DM
378 * Starts the test mode checking the composer installation and
379 * the phpunit test environment and updating the available
380 * features and steps definitions.
381 *
a09534f4 382 * Stores a file in dataroot/behat to allow Moodle to switch
bd770617 383 * to the test environment when using cli-server (or $CFG->behat_switchcompletely)
7f541ea3
DM
384 *
385 * @throws file_exception
386 */
d6abac76 387 protected static function start_test_mode() {
554820dd 388 global $CFG;
7f541ea3 389
0b5f481b
DM
390 // Checks the behat set up and the PHP version.
391 self::check_behat_setup(true);
392
393 // Check that PHPUnit test environment is correctly set up.
394 self::test_environment_problem();
395
396 // Updates all the Moodle features and steps definitions.
397 self::update_config_file();
398
cca2c806 399 if (self::is_test_mode_enabled()) {
7f541ea3
DM
400 debugging('Test environment was already enabled');
401 return;
402 }
403
8cdc0ce8 404 $behatdir = self::get_behat_dir();
7f541ea3 405
bd770617 406 $contents = '$CFG->behat_wwwroot, $CFG->phpunit_prefix and $CFG->phpunit_dataroot' .
d46340eb 407 ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
7f541ea3 408 $filepath = $behatdir . '/test_environment_enabled.txt';
33005f68 409 if (!file_put_contents($filepath, $contents)) {
7f541ea3
DM
410 throw new file_exception('cannotcreatefile', $filepath);
411 }
554820dd 412 chmod($filepath, $CFG->directorypermissions);
7f541ea3
DM
413 }
414
415 /**
416 * Disables test mode
a09534f4 417 * @throws file_exception
7f541ea3 418 */
d6abac76 419 protected static function stop_test_mode() {
7f541ea3 420
33005f68 421 $testenvfile = self::get_test_filepath();
7f541ea3 422
cca2c806 423 if (!self::is_test_mode_enabled()) {
7f541ea3
DM
424 debugging('Test environment was already disabled');
425 } else {
cc544646
DM
426 if (!unlink($testenvfile)) {
427 throw new file_exception('cannotdeletetestenvironmentfile');
428 }
7f541ea3
DM
429 }
430 }
431
432 /**
433 * Checks whether test environment is enabled or disabled
cc544646 434 *
a09534f4
DM
435 * To check is the current script is running in the test
436 * environment {@see tool_behat::is_test_environment_running()}
cc544646
DM
437 *
438 * @return bool
7f541ea3 439 */
a09534f4 440 public static function is_test_mode_enabled() {
7f541ea3 441
33005f68 442 $testenvfile = self::get_test_filepath();
7f541ea3
DM
443 if (file_exists($testenvfile)) {
444 return true;
445 }
446
447 return false;
448 }
449
cc544646
DM
450 /**
451 * Returns true if Moodle is currently running with the test database and dataroot
452 * @return bool
453 */
d6abac76 454 public static function is_test_environment_running() {
cc544646
DM
455 global $CFG;
456
0b5f481b 457 if (!empty($CFG->originaldataroot)) {
cc544646
DM
458 return true;
459 }
460
461 return false;
462 }
463
d6abac76
DM
464 /**
465 * Has the site installed composer with --dev option
466 * @return boolean
467 */
468 public static function are_behat_dependencies_installed() {
469 if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
470 return false;
471 }
472 return true;
473 }
474
cc544646
DM
475 /**
476 * Returns the path to the file which specifies if test environment is enabled
0b5f481b
DM
477 *
478 * The file is in dataroot/behat/ but we need to
479 * know if test mode is running because then we swap
480 * it to phpunit_dataroot and we need the original value
481 *
cc544646
DM
482 * @return string
483 */
d6abac76 484 protected static function get_test_filepath() {
cc544646
DM
485 global $CFG;
486
33005f68 487 if (self::is_test_environment_running()) {
0b5f481b 488 $prefix = $CFG->originaldataroot;
cc544646 489 } else {
0b5f481b 490 $prefix = $CFG->dataroot;
cc544646
DM
491 }
492
0b5f481b 493 return $prefix . '/behat/test_environment_enabled.txt';
cc544646
DM
494 }
495
0922ef87
DM
496
497 /**
498 * The relative path where components stores their behat tests
499 *
500 * @return string
501 */
502 protected static function get_behat_tests_path() {
503 return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
504 }
505
8cdc0ce8
DM
506 /**
507 * Ensures the behat dir exists in moodledata
508 * @throws file_exception
509 * @return string Full path
510 */
d6abac76 511 protected static function get_behat_dir() {
8cdc0ce8
DM
512 global $CFG;
513
514 $behatdir = $CFG->dataroot . '/behat';
515
516 if (!is_dir($behatdir)) {
517 if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
518 throw new file_exception('storedfilecannotcreatefiledirs');
519 }
520 }
521
522 if (!is_writable($behatdir)) {
523 throw new file_exception('storedfilecannotcreatefiledirs');
524 }
525
526 return $behatdir;
527 }
528
df8dcf3e 529 /**
0b5f481b
DM
530 * Returns the executable path
531 * @return string
532 */
d6abac76 533 protected static function get_behat_command() {
0b5f481b
DM
534 return 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'behat';
535 }
536
537 /**
538 * Returns the behat config file path used by the steps definition list
df8dcf3e
DM
539 * @return string
540 */
d6abac76 541 protected static function get_steps_list_config_filepath() {
df8dcf3e
DM
542 return self::get_behat_dir() . '/behat.yml';
543 }
544
7f541ea3 545}