Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
 * Wrapper to run previously set-up behat tests in parallel.
19
 *
20
 * @package    tool_behat
21
 * @copyright  2014 NetSpot Pty Ltd
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
if (isset($_SERVER['REMOTE_ADDR'])) {
26
    die(); // No access from web!
27
}
28
 
29
define('CLI_SCRIPT', true);
30
define('ABORT_AFTER_CONFIG', true);
31
define('CACHE_DISABLE_ALL', true);
32
define('NO_OUTPUT_BUFFERING', true);
33
 
1441 ariadna 34
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
35
// from the normal environment), so in case user has set tne environment variable, disable it.
36
putenv('BEHAT_CLI=0');
37
 
1 efrain 38
require_once(__DIR__ .'/../../../../config.php');
39
require_once(__DIR__.'/../../../../lib/clilib.php');
40
require_once(__DIR__.'/../../../../lib/behat/lib.php');
41
require_once(__DIR__.'/../../../../lib/behat/classes/behat_command.php');
42
require_once(__DIR__.'/../../../../lib/behat/classes/behat_config_manager.php');
43
 
1441 ariadna 44
error_reporting(E_ALL);
1 efrain 45
ini_set('display_errors', '1');
46
ini_set('log_errors', '1');
47
 
48
list($options, $unrecognised) = cli_get_params(
49
    array(
50
        'stop-on-failure' => 0,
51
        'verbose'  => false,
52
        'replace'  => '',
53
        'help'     => false,
54
        'tags'     => '',
55
        'profile'  => '',
56
        'feature'  => '',
57
        'suite'    => '',
58
        'fromrun'  => 1,
59
        'torun'    => 0,
60
        'single-run' => false,
61
        'rerun' => 0,
62
        'auto-rerun' => 0,
63
    ),
64
    array(
65
        'h' => 'help',
66
        't' => 'tags',
67
        'p' => 'profile',
68
        's' => 'single-run',
69
    )
70
);
71
 
72
// Checking run.php CLI script usage.
73
$help = "
74
Behat utilities to run behat tests in parallel
75
 
76
Usage:
77
  php run.php [--BEHAT_OPTION=\"value\"] [--feature=\"value\"] [--replace=\"{run}\"] [--fromrun=value --torun=value] [--help]
78
 
79
Options:
80
--BEHAT_OPTION     Any combination of behat option specified in http://behat.readthedocs.org/en/v2.5/guides/6.cli.html
81
--feature          Only execute specified feature file (Absolute path of feature file).
82
--suite            Specified theme scenarios will be executed.
83
--replace          Replace args string with run process number, useful for output.
84
--fromrun          Execute run starting from (Used for parallel runs on different vms)
85
--torun            Execute run till (Used for parallel runs on different vms)
86
--rerun            Re-run scenarios that failed during last execution.
87
--auto-rerun       Automatically re-run scenarios that failed during last execution.
88
 
89
-h, --help         Print out this help
90
 
91
Example from Moodle root directory:
92
\$ php admin/tool/behat/cli/run.php --tags=\"@javascript\"
93
 
94
More info in https://moodledev.io/general/development/tools/behat/running
95
";
96
 
97
if (!empty($options['help'])) {
98
    echo $help;
99
    exit(0);
100
}
101
 
102
$parallelrun = behat_config_manager::get_behat_run_config_value('parallel');
103
 
104
// Check if the options provided are valid to run behat.
105
if ($parallelrun === false) {
106
    // Parallel run should not have fromrun or torun options greater than 1.
107
    if (($options['fromrun'] > 1) || ($options['torun'] > 1)) {
108
        echo "Test site is not initialized  for parallel run." . PHP_EOL;
109
        exit(1);
110
    }
111
} else {
112
    // Ensure fromrun is within limits of initialized test site.
113
    if (!empty($options['fromrun']) && ($options['fromrun'] > $parallelrun)) {
114
        echo "From run (" . $options['fromrun'] . ") is more than site with parallel runs (" . $parallelrun . ")" . PHP_EOL;
115
        exit(1);
116
    }
117
 
118
    // Default torun is maximum parallel runs and should be less than equal to parallelruns.
119
    if (empty($options['torun'])) {
120
        $options['torun'] = $parallelrun;
121
    } else {
122
        if ($options['torun'] > $parallelrun) {
123
            echo "To run (" . $options['torun'] . ") is more than site with parallel runs (" . $parallelrun . ")" . PHP_EOL;
124
            exit(1);
125
        }
126
    }
127
}
128
 
129
// Capture signals and ensure we clean symlinks.
130
if (extension_loaded('pcntl')) {
131
    $disabled = explode(',', ini_get('disable_functions'));
132
    if (!in_array('pcntl_signal', $disabled)) {
133
        pcntl_signal(SIGTERM, "signal_handler");
134
        pcntl_signal(SIGINT, "signal_handler");
135
    }
136
}
137
 
138
$time = microtime(true);
139
array_walk($unrecognised, function (&$v) {
140
    if ($x = preg_filter("#^(-+\w+)=(.+)#", "\$1=\"\$2\"", $v)) {
141
        $v = $x;
142
    } else if (!preg_match("#^-#", $v)) {
143
        $v = escapeshellarg($v);
144
    }
145
});
146
$extraopts = $unrecognised;
147
 
148
if ($options['profile']) {
149
    $profile = $options['profile'];
150
 
151
    // If profile passed is not set, then exit (note we skip if the 'replace' option is found within the 'profile' value).
152
    if (!isset($CFG->behat_config[$profile]) && !isset($CFG->behat_profiles[$profile]) &&
153
            !($options['replace'] && (strpos($profile, (string) $options['replace']) !== false))) {
154
 
155
        echo "Invalid profile passed: " . $profile . PHP_EOL;
156
        exit(1);
157
    }
158
 
159
    $extraopts['profile'] = '--profile="' . $profile . '"';
160
    // By default, profile tags will be used.
161
    if (!empty($CFG->behat_config[$profile]['filters']['tags'])) {
162
        $tags = $CFG->behat_config[$profile]['filters']['tags'];
163
    }
164
}
165
 
166
// Command line tags have precedence (std behat behavior).
167
if ($options['tags']) {
168
    $tags = $options['tags'];
169
    $extraopts['tags'] = '--tags="' . $tags . '"';
170
}
171
 
172
// Add suite option if specified.
173
if ($options['suite']) {
174
    $extraopts['suite'] = '--suite="' . $options['suite'] . '"';
175
}
176
 
177
// Feature should be added to last, for behat command.
178
if ($options['feature']) {
179
    $extraopts['feature'] = $options['feature'];
180
    // Only run 1 process as process.
181
    // Feature file is picked from absolute path provided, so no need to check for behat.yml.
182
    $options['torun'] = $options['fromrun'];
183
}
184
 
185
// Set of options to pass to behat.
186
$extraoptstr = implode(' ', $extraopts);
187
 
188
// If rerun is passed then ensure we just run the failed processes.
189
$lastfailedstatus = 0;
190
$lasttorun = $options['torun'];
191
$lastfromrun = $options['fromrun'];
192
if ($options['rerun']) {
193
    // Get last combined failed status.
194
    $lastfailedstatus = behat_config_manager::get_behat_run_config_value('lastcombinedfailedstatus');
195
    $lasttorun = behat_config_manager::get_behat_run_config_value('lasttorun');
196
    $lastfromrun = behat_config_manager::get_behat_run_config_value('lastfromrun');
197
 
198
    if ($lastfailedstatus !== false) {
199
        $extraoptstr .= ' --rerun';
200
    }
201
 
202
    // If torun is less than last torun, then just set this to min last to run and similar for fromrun.
203
    if ($options['torun'] < $lasttorun) {
204
        $options['torun'];
205
    }
206
    if ($options['fromrun'] > $lastfromrun) {
207
        $options['fromrun'];
208
    }
209
    unset($options['rerun']);
210
}
211
 
212
$cmds = array();
213
$exitcodes = array();
214
$status = 0;
215
$verbose = empty($options['verbose']) ? false : true;
216
 
217
// Execute behat run commands.
218
if (empty($parallelrun)) {
219
    $cwd = getcwd();
220
    chdir(__DIR__);
221
    $runtestscommand = behat_command::get_behat_command(false, false, true);
222
    $runtestscommand .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
223
    $runtestscommand .= ' ' . $extraoptstr;
224
    $cmds['singlerun'] = $runtestscommand;
225
 
226
    echo "Running single behat site:" . PHP_EOL;
227
    passthru("php $runtestscommand", $status);
228
    $exitcodes['singlerun'] = $status;
229
    chdir($cwd);
230
} else {
231
 
232
    echo "Running " . ($options['torun'] - $options['fromrun'] + 1) . " parallel behat sites:" . PHP_EOL;
233
 
234
    for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
235
        $lastfailed = 1 & $lastfailedstatus >> ($i - 1);
236
 
237
        // Bypass if not failed in last run.
238
        if ($lastfailedstatus && !$lastfailed && ($i <= $lasttorun) && ($i >= $lastfromrun)) {
239
            continue;
240
        }
241
 
242
        $CFG->behatrunprocess = $i;
243
 
244
        // Options parameters to be added to each run.
245
        $myopts = !empty($options['replace']) ? str_replace($options['replace'], $i, $extraoptstr) : $extraoptstr;
246
 
247
        $behatcommand = behat_command::get_behat_command(false, false, true);
248
        $behatconfigpath = behat_config_manager::get_behat_cli_config_filepath($i);
249
 
250
        // Command to execute behat run.
251
        $cmds[BEHAT_PARALLEL_SITE_NAME . $i] = $behatcommand . ' --config ' . $behatconfigpath . " " . $myopts;
252
        echo "[" . BEHAT_PARALLEL_SITE_NAME . $i . "] " . $cmds[BEHAT_PARALLEL_SITE_NAME . $i] . PHP_EOL;
253
    }
254
 
255
    if (empty($cmds)) {
256
        echo "No commands to execute " . PHP_EOL;
257
        exit(1);
258
    }
259
 
260
    // Create site symlink if necessary.
261
    if (!behat_config_manager::create_parallel_site_links($options['fromrun'], $options['torun'])) {
262
        echo "Check permissions. If on windows, make sure you are running this command as admin" . PHP_EOL;
263
        exit(1);
264
    }
265
 
266
    // Save torun and from run, so it can be used to detect if it was executed in last run.
267
    behat_config_manager::set_behat_run_config_value('lasttorun', $options['torun']);
268
    behat_config_manager::set_behat_run_config_value('lastfromrun', $options['fromrun']);
269
 
270
    // Keep no delay by default, between each parallel, let user decide.
271
    if (!defined('BEHAT_PARALLEL_START_DELAY')) {
272
        define('BEHAT_PARALLEL_START_DELAY', 0);
273
    }
274
 
275
    // Execute all commands, relative to moodle root directory.
276
    $processes = cli_execute_parallel($cmds, __DIR__ . "/../../../../", BEHAT_PARALLEL_START_DELAY);
277
    $stoponfail = empty($options['stop-on-failure']) ? false : true;
278
 
279
    // Print header.
280
    print_process_start_info($processes);
281
 
282
    // Print combined run o/p from processes.
283
    $exitcodes = print_combined_run_output($processes, $stoponfail);
284
    // Time to finish run.
285
    $time = round(microtime(true) - $time, 0);
286
    echo "Finished in " . gmdate("G\h i\m s\s", $time) . PHP_EOL . PHP_EOL;
287
    ksort($exitcodes);
288
 
289
    // Print exit info from each run.
290
    // Status bits contains pass/fail status of parallel runs.
291
    foreach ($exitcodes as $name => $exitcode) {
292
        if ($exitcode) {
293
            $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
294
            $status |= (1 << ($runno - 1));
295
        }
296
    }
297
 
298
    // Print each process information.
299
    print_each_process_info($processes, $verbose, $status);
300
}
301
 
302
// Save final exit code containing which run failed.
303
behat_config_manager::set_behat_run_config_value('lastcombinedfailedstatus', $status);
304
 
305
// Show exit code from each process, if any process failed and how to rerun failed process.
306
if ($verbose || $status) {
307
    // Check if status of last run is failure and rerun is suggested.
308
    if (!empty($options['auto-rerun']) && $status) {
309
        // Rerun for the number of tries passed.
310
        for ($i = 0; $i < $options['auto-rerun']; $i++) {
311
 
312
            // Run individual commands, to avoid parallel failures.
313
            foreach ($exitcodes as $behatrunname => $exitcode) {
314
                // If not failed in last run, then skip.
315
                if ($exitcode == 0) {
316
                    continue;
317
                }
318
 
319
                // This was a failure.
320
                echo "*** Re-running behat run: $behatrunname ***" . PHP_EOL;
321
                if ($verbose) {
322
                    echo "Executing: " . $cmds[$behatrunname] . " --rerun" . PHP_EOL;
323
                }
324
 
325
                passthru("php $cmds[$behatrunname] --rerun", $rerunstatus);
326
 
327
                // Update exit code.
328
                $exitcodes[$behatrunname] = $rerunstatus;
329
            }
330
        }
331
 
332
        // Update status after auto-rerun finished.
333
        $status = 0;
334
        foreach ($exitcodes as $name => $exitcode) {
335
            if ($exitcode) {
336
                if (!empty($parallelrun)) {
337
                    $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
338
                } else {
339
                    $runno = 1;
340
                }
341
                $status |= (1 << ($runno - 1));
342
            }
343
        }
344
    }
345
 
346
    // Show final o/p with re-run commands.
347
    if ($status) {
348
        if (!empty($parallelrun)) {
349
            // Echo exit codes.
350
            echo "Exit codes for each behat run: " . PHP_EOL;
351
            foreach ($exitcodes as $run => $exitcode) {
352
                echo $run . ": " . $exitcode . PHP_EOL;
353
            }
354
            unset($extraopts['fromrun']);
355
            unset($extraopts['torun']);
356
            if (!empty($options['replace'])) {
357
                $extraopts['replace'] = '--replace="' . $options['replace'] . '"';
358
            }
359
        }
360
 
361
        echo "To re-run failed processes, you can use following command:" . PHP_EOL;
362
        $extraopts['rerun'] = '--rerun';
363
        $extraoptstr = implode(' ', $extraopts);
364
        echo behat_command::get_behat_command(true, true, true) . " " . $extraoptstr . PHP_EOL;
365
    }
366
    echo PHP_EOL;
367
}
368
 
369
// Remove site symlink if necessary.
370
behat_config_manager::drop_parallel_site_links();
371
 
372
exit($status);
373
 
374
/**
375
 * Signal handler for terminal exit.
376
 *
377
 * @param int $signal signal number.
378
 */
379
function signal_handler($signal) {
380
    switch ($signal) {
381
        case SIGTERM:
382
        case SIGKILL:
383
        case SIGINT:
384
            // Remove site symlink if necessary.
385
            behat_config_manager::drop_parallel_site_links();
386
            exit(1);
387
    }
388
}
389
 
390
/**
391
 * Prints header from the first process.
392
 *
393
 * @param array $processes list of processes to loop though.
394
 */
395
function print_process_start_info($processes) {
396
    $printed = false;
397
    // Keep looping though processes, till we get first process o/p.
398
    while (!$printed) {
399
        usleep(10000);
400
        foreach ($processes as $name => $process) {
401
            // Exit if any process has stopped.
402
            if (!$process->isRunning()) {
403
                $printed = true;
404
                break;
405
            }
406
 
407
            $op = explode(PHP_EOL, $process->getOutput());
408
            if (count($op) >= 3) {
409
                foreach ($op as $line) {
410
                    if (trim($line) && (strpos($line, '.') !== 0)) {
411
                        echo $line . PHP_EOL;
412
                    }
413
                }
414
                $printed = true;
415
            }
416
        }
417
    }
418
}
419
 
420
/**
421
 * Loop though all processes and print combined o/p
422
 *
423
 * @param array $processes list of processes to loop though.
424
 * @param bool $stoponfail Stop all processes and exit if failed.
425
 * @return array list of exit codes from all processes.
426
 */
427
function print_combined_run_output($processes, $stoponfail = false) {
428
    $exitcodes = array();
429
    $maxdotsonline = 70;
430
    $remainingprintlen = $maxdotsonline;
431
    $progresscount = 0;
432
    while (count($exitcodes) != count($processes)) {
433
        usleep(10000);
434
        foreach ($processes as $name => $process) {
435
            if ($process->isRunning()) {
436
                $op = $process->getIncrementalOutput();
437
                if (trim($op)) {
438
                    $update = preg_filter('#^\s*([FS\.\-]+)(?:\s+\d+)?\s*$#', '$1', $op);
439
                    // Exit process if anything fails.
440
                    if ($stoponfail && (strpos($update, 'F') !== false)) {
441
                        $process->stop(0);
442
                    }
443
 
444
                    $strlentoprint = strlen($update ?? '');
445
 
446
                    // If not enough dots printed on line then just print.
447
                    if ($strlentoprint < $remainingprintlen) {
448
                        echo $update;
449
                        $remainingprintlen = $remainingprintlen - $strlentoprint;
450
                    } else if ($strlentoprint == $remainingprintlen) {
451
                        $progresscount += $maxdotsonline;
452
                        echo $update ." " . $progresscount . PHP_EOL;
453
                        $remainingprintlen = $maxdotsonline;
454
                    } else {
455
                        while ($part = substr($update, 0, $remainingprintlen) > 0) {
456
                            $progresscount += $maxdotsonline;
457
                            echo $part . " " . $progresscount . PHP_EOL;
458
                            $update = substr($update, $remainingprintlen);
459
                            $remainingprintlen = $maxdotsonline;
460
                        }
461
                    }
462
                }
463
            } else {
464
                $exitcodes[$name] = $process->getExitCode();
465
                if ($stoponfail && ($exitcodes[$name] != 0)) {
466
                    foreach ($processes as $l => $p) {
467
                        $exitcodes[$l] = -1;
468
                        $process->stop(0);
469
                    }
470
                }
471
            }
472
        }
473
    }
474
 
475
    echo PHP_EOL;
476
    return $exitcodes;
477
}
478
 
479
/**
480
 * Loop though all processes and print combined o/p
481
 *
482
 * @param array $processes list of processes to loop though.
483
 * @param bool $verbose Show verbose output for each process.
484
 */
485
function print_each_process_info($processes, $verbose = false, $status = 0) {
486
    foreach ($processes as $name => $process) {
487
        echo "**************** [" . $name . "] ****************" . PHP_EOL;
488
        if ($verbose) {
489
            echo $process->getOutput();
490
            echo $process->getErrorOutput();
491
 
492
        } else if ($status) {
493
            // Only show failed o/p.
494
            $runno = str_replace(BEHAT_PARALLEL_SITE_NAME, '', $name);
495
            if ((1 << ($runno - 1)) & $status) {
496
                echo $process->getOutput();
497
                echo $process->getErrorOutput();
498
            } else {
499
                echo get_status_lines_from_run_op($process);
500
            }
501
 
502
        } else {
503
            echo get_status_lines_from_run_op($process);
504
        }
505
        echo PHP_EOL;
506
    }
507
}
508
 
509
/**
510
 * Extract status information from behat o/p and return.
511
 * @param Symfony\Component\Process\Process $process
512
 * @return string
513
 */
514
function get_status_lines_from_run_op(Symfony\Component\Process\Process $process) {
515
    $statusstr = '';
516
    $op = explode(PHP_EOL, $process->getOutput());
517
    foreach ($op as $line) {
518
        // Don't print progress .
519
        if (trim($line) && (strpos($line, '.') !== 0) && (strpos($line, 'Moodle ') !== 0) &&
520
            (strpos($line, 'Server OS ') !== 0) && (strpos($line, 'Started at ') !== 0) &&
521
            (strpos($line, 'Browser specific fixes ') !== 0)) {
522
            $statusstr .= $line . PHP_EOL;
523
        }
524
    }
525
 
526
    return $statusstr;
527
}
528