Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 403]

   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  namespace Moodle\BehatExtension\Output\Formatter;
  19  
  20  use Behat\Behat\EventDispatcher\Event\AfterStepTested;
  21  use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested;
  22  use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
  23  use Behat\Testwork\Output\Formatter;
  24  use Behat\Testwork\Output\Printer\OutputPrinter;
  25  
  26  // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
  27  
  28  /**
  29   * Feature step counter for distributing features between parallel runs.
  30   *
  31   * Use it with --dry-run (and any other selectors combination) to
  32   * get the results quickly.
  33   *
  34   * @package core
  35   * @copyright  2016 onwards Rajesh Taneja
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class MoodleScreenshotFormatter implements Formatter {
  39  
  40      /** @var OutputPrinter */
  41      private $printer;
  42  
  43      /** @var array */
  44      private $parameters;
  45  
  46      /** @var string */
  47      private $name;
  48  
  49      /** @var string */
  50      private $description;
  51  
  52      /** @var int The scenario count */
  53      protected static $currentscenariocount = 0;
  54  
  55      /** @var int The step count within the current scenario */
  56      protected static $currentscenariostepcount = 0;
  57  
  58      /**
  59       * If we are saving any kind of dump on failure we should use the same parent dir during a run.
  60       *
  61       * @var The parent dir name
  62       */
  63      protected static $faildumpdirname = false;
  64  
  65      /**
  66       * Initializes formatter.
  67       *
  68       * @param string        $name
  69       * @param string        $description
  70       * @param array         $parameters
  71       * @param OutputPrinter $printer
  72       */
  73      public function __construct($name, $description, array $parameters, OutputPrinter $printer) {
  74          $this->name = $name;
  75          $this->description = $description;
  76          $this->parameters = $parameters;
  77          $this->printer = $printer;
  78      }
  79  
  80      /**
  81       * Returns an array of event names this subscriber wants to listen to.
  82       *
  83       * @return array The event names to listen to
  84       */
  85      public static function getSubscribedEvents() {
  86          return [
  87              'tester.scenario_tested.before'    => 'beforeScenario',
  88              'tester.step_tested.before'        => 'beforeStep',
  89              'tester.step_tested.after'         => 'afterStep',
  90          ];
  91      }
  92  
  93      /**
  94       * Returns formatter name.
  95       *
  96       * @return string
  97       */
  98      public function getName() {
  99          return $this->name;
 100      }
 101  
 102      /**
 103       * Returns formatter description.
 104       *
 105       * @return string
 106       */
 107      public function getDescription() {
 108          return $this->description;
 109      }
 110  
 111      /**
 112       * Returns formatter output printer.
 113       *
 114       * @return OutputPrinter
 115       */
 116      public function getOutputPrinter() {
 117          return $this->printer;
 118      }
 119  
 120      /**
 121       * Sets formatter parameter.
 122       *
 123       * @param string $name
 124       * @param mixed  $value
 125       */
 126      public function setParameter($name, $value) {
 127          $this->parameters[$name] = $value;
 128      }
 129  
 130      /**
 131       * Returns parameter name.
 132       *
 133       * @param string $name
 134       * @return mixed
 135       */
 136      public function getParameter($name) {
 137          return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
 138      }
 139  
 140      /**
 141       * Reset currentscenariostepcount
 142       *
 143       * @param BeforeScenarioTested $event
 144       */
 145      public function beforeScenario(BeforeScenarioTested $event) {
 146  
 147          self::$currentscenariostepcount = 0;
 148          self::$currentscenariocount++;
 149      }
 150  
 151      /**
 152       * Increment currentscenariostepcount
 153       *
 154       * @param BeforeStepTested $event
 155       */
 156      public function beforeStep(BeforeStepTested $event) {
 157          self::$currentscenariostepcount++;
 158      }
 159  
 160      /**
 161       * Take screenshot after step is executed.    Behat\Behat\Event\html
 162       *
 163       * @param AfterStepTested $event
 164       */
 165      public function afterStep(AfterStepTested $event) {
 166          $behathookcontext = $event->getEnvironment()->getContext('behat_hooks');
 167  
 168          $formats = $this->getParameter('formats');
 169          $formats = explode(',', $formats);
 170  
 171          // Take screenshot.
 172          if (in_array('image', $formats)) {
 173              $this->take_screenshot($event, $behathookcontext);
 174          }
 175  
 176          // Save html content.
 177          if (in_array('html', $formats)) {
 178              $this->take_contentdump($event, $behathookcontext);
 179          }
 180      }
 181  
 182      /**
 183       * Return screenshot directory where all screenshots will be saved.
 184       *
 185       * @return string
 186       */
 187      protected function get_run_screenshot_dir() {
 188          global $CFG;
 189  
 190          if (self::$faildumpdirname) {
 191              return self::$faildumpdirname;
 192          }
 193  
 194          // If output_path is set then use output_path else use faildump_path.
 195          if ($this->getOutputPrinter()->getOutputPath()) {
 196              $screenshotpath = $this->getOutputPrinter()->getOutputPath();
 197          } else if ($CFG->behat_faildump_path) {
 198              $screenshotpath = $CFG->behat_faildump_path;
 199          } else {
 200              // It should never reach here.
 201              throw new FormatterException('You should specify --out "SOME/PATH" for moodle_screenshot format');
 202          }
 203  
 204          if ($this->getParameter('dir_permissions')) {
 205              $dirpermissions = $this->getParameter('dir_permissions');
 206          } else {
 207              $dirpermissions = 0777;
 208          }
 209  
 210          // All the screenshot dumps should be in the same parent dir.
 211          self::$faildumpdirname = $screenshotpath . DIRECTORY_SEPARATOR . date('Ymd_His');
 212  
 213          if (!is_dir(self::$faildumpdirname) && !mkdir(self::$faildumpdirname, $dirpermissions, true)) {
 214              // It shouldn't, we already checked that the directory is writable.
 215              throw new FormatterException(sprintf(
 216                  'No directories can be created inside %s, check the directory permissions.', $screenshotpath
 217              ));
 218          }
 219  
 220          return self::$faildumpdirname;
 221      }
 222  
 223      /**
 224       * Take screenshot when a step fails.
 225       *
 226       * @throws Exception
 227       * @param AfterStepTested $event
 228       * @param Context $context
 229       */
 230      protected function take_screenshot(AfterStepTested $event, $context) {
 231          // Goutte can't save screenshots.
 232          if ($context->getMink()->isSessionStarted($context->getMink()->getDefaultSessionName())) {
 233              if (get_class($context->getMink()->getSession()->getDriver()) === 'Behat\Mink\Driver\GoutteDriver') {
 234                  return false;
 235              }
 236              list ($dir, $filename) = $this->get_faildump_filename($event, 'png');
 237              $context->saveScreenshot($filename, $dir);
 238          }
 239      }
 240  
 241      /**
 242       * Take a dump of the page content when a step fails.
 243       *
 244       * @throws Exception
 245       * @param AfterStepTested $event
 246       * @param \Behat\Context\Context\Context $context
 247       */
 248      protected function take_contentdump(AfterStepTested $event, $context) {
 249          list ($dir, $filename) = $this->get_faildump_filename($event, 'html');
 250          $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
 251          fwrite($fh, $context->getMink()->getSession()->getPage()->getContent());
 252          fclose($fh);
 253      }
 254  
 255      /**
 256       * Determine the full pathname to store a failure-related dump.
 257       *
 258       * This is used for content such as the DOM, and screenshots.
 259       *
 260       * @param AfterStepTested $event
 261       * @param String $filetype The file suffix to use. Limited to 4 chars.
 262       */
 263      protected function get_faildump_filename(AfterStepTested $event, $filetype) {
 264          // Make a directory for the scenario.
 265          $featurename = $event->getFeature()->getTitle();
 266          $featurename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $featurename);
 267          if ($this->getParameter('dir_permissions')) {
 268              $dirpermissions = $this->getParameter('dir_permissions');
 269          } else {
 270              $dirpermissions = 0777;
 271          }
 272  
 273          $dir = $this->get_run_screenshot_dir();
 274  
 275          // We want a i-am-the-scenario-title format.
 276          $dir = $dir . DIRECTORY_SEPARATOR . self::$currentscenariocount . '-' . $featurename;
 277          if (!is_dir($dir) && !mkdir($dir, $dirpermissions, true)) {
 278              // We already checked that the directory is writable. This should not fail.
 279              throw new FormatterException(sprintf(
 280                  'No directories can be created inside %s, check the directory permissions.', $dir
 281              ));
 282          }
 283  
 284          // The failed step text.
 285          // We want a stepno-i-am-the-failed-step.$filetype format.
 286          $filename = $event->getStep()->getText();
 287          $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
 288          $filename = self::$currentscenariostepcount . '-' . $filename;
 289  
 290          // File name limited to 255 characters. Leaving 4 chars for the file
 291          // extension as we allow .png for images and .html for DOM contents.
 292          $filename = substr($filename, 0, 250) . '.' . $filetype;
 293          return [$dir, $filename];
 294      }
 295  }