Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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  namespace Moodle\BehatExtension\Tester\Cli;
  18  
  19  use Behat\Behat\EventDispatcher\Event\AfterScenarioTested;
  20  use Behat\Behat\EventDispatcher\Event\ExampleTested;
  21  use Behat\Behat\EventDispatcher\Event\ScenarioTested;
  22  use Behat\Testwork\Cli\Controller;
  23  use Behat\Testwork\EventDispatcher\Event\ExerciseCompleted;
  24  use Behat\Testwork\Tester\Result\TestResult;
  25  use Symfony\Component\Console\Command\Command;
  26  use Symfony\Component\Console\Input\InputInterface;
  27  use Symfony\Component\Console\Input\InputOption;
  28  use Symfony\Component\Console\Output\OutputInterface;
  29  use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  30  
  31  // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
  32  
  33  /**
  34   * Caches passed scenarios and skip only them if `--skip-passed` option provided.
  35   *
  36   * @package core
  37   * @copyright  2016 onwards Rajesh Taneja
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  final class SkipPassedController implements Controller {
  41      /**
  42       * @var EventDispatcherInterface
  43       */
  44      private $eventdispatcher;
  45  
  46      /**
  47       * @var null|string
  48       */
  49      private $cachepath;
  50  
  51      /**
  52       * @var string
  53       */
  54      private $key;
  55  
  56      /**
  57       * @var string[]
  58       */
  59      private $lines = [];
  60  
  61      /**
  62       * @var string
  63       */
  64      private $basepath;
  65  
  66      /**
  67       * Initializes controller.
  68       *
  69       * @param EventDispatcherInterface $eventdispatcher
  70       * @param null|string              $cachepath
  71       * @param string                   $basepath
  72       */
  73      public function __construct(EventDispatcherInterface $eventdispatcher, $cachepath, $basepath) {
  74          $this->eventdispatcher = $eventdispatcher;
  75          $this->cachepath = null !== $cachepath ? rtrim($cachepath, DIRECTORY_SEPARATOR) : null;
  76          $this->basepath = $basepath;
  77      }
  78  
  79      /**
  80       * Configures command to be executable by the controller.
  81       *
  82       * @param Command $command
  83       */
  84      public function configure(Command $command) {
  85          $command->addOption('--skip-passed', null, InputOption::VALUE_NONE,
  86              'Skip scenarios that passed during last execution.'
  87          );
  88      }
  89  
  90      /**
  91       * Executes controller.
  92       *
  93       * @param InputInterface  $input
  94       * @param OutputInterface $output
  95       *
  96       * @return null|integer
  97       */
  98      public function execute(InputInterface $input, OutputInterface $output) {
  99          if (!$input->getOption('skip-passed')) {
 100              // If no skip option is passed then remove any old file which we are saving.
 101              if (!$this->getFileName()) {
 102                  return;
 103              }
 104              if (file_exists($this->getFileName())) {
 105                  unlink($this->getFileName());
 106              }
 107              return;
 108          }
 109  
 110          $this->eventdispatcher->addListener(ScenarioTested::AFTER, [$this, 'collectPassedScenario'], -50);
 111          $this->eventdispatcher->addListener(ExampleTested::AFTER, [$this, 'collectPassedScenario'], -50);
 112          $this->eventdispatcher->addListener(ExerciseCompleted::AFTER, [$this, 'writeCache'], -50);
 113          $this->key = $this->generateKey($input);
 114  
 115          if (!$this->getFileName() || !file_exists($this->getFileName())) {
 116              return;
 117          }
 118          $input->setArgument('paths', $this->getFileName());
 119  
 120          $existing = json_decode(file_get_contents($this->getFileName()), true);
 121          if (!empty($existing)) {
 122              $this->lines = array_merge_recursive($existing, $this->lines);
 123          }
 124      }
 125  
 126      /**
 127       * Records scenario if it is passed.
 128       *
 129       * @param AfterScenarioTested $event
 130       */
 131      public function collectPassedScenario(AfterScenarioTested $event) {
 132          if (!$this->getFileName()) {
 133              return;
 134          }
 135  
 136          $feature = $event->getFeature();
 137          $suitename = $event->getSuite()->getName();
 138  
 139          if (
 140              ($event->getTestResult()->getResultCode() !== TestResult::PASSED) &&
 141              ($event->getTestResult()->getResultCode() !== TestResult::SKIPPED)
 142          ) {
 143              unset($this->lines[$suitename][$feature->getFile()]);
 144              return;
 145          }
 146  
 147          $this->lines[$suitename][$feature->getFile()] = $feature->getFile();
 148      }
 149  
 150      /**
 151       * Writes passed scenarios cache.
 152       */
 153      public function writeCache() {
 154          if (!$this->getFileName()) {
 155              return;
 156          }
 157          if (0 === count($this->lines)) {
 158              return;
 159          }
 160          file_put_contents($this->getFileName(), json_encode($this->lines));
 161      }
 162  
 163      /**
 164       * Generates cache key.
 165       *
 166       * @param InputInterface $input
 167       *
 168       * @return string
 169       */
 170      private function generateKey(InputInterface $input) {
 171          return md5(
 172              $input->getParameterOption(['--profile', '-p']) .
 173              $input->getOption('suite') .
 174              implode(' ', $input->getOption('name')) .
 175              implode(' ', $input->getOption('tags')) .
 176              $input->getOption('role') .
 177              $input->getArgument('paths') .
 178              $this->basepath
 179          );
 180      }
 181  
 182      /**
 183       * Returns cache filename (if exists).
 184       *
 185       * @return null|string
 186       */
 187      private function getFileName() {
 188          if (null === $this->cachepath || null === $this->key) {
 189              return null;
 190          }
 191          if (!is_dir($this->cachepath)) {
 192              mkdir($this->cachepath, 0777);
 193          }
 194          return $this->cachepath . DIRECTORY_SEPARATOR . $this->key . '.passed';
 195      }
 196  }