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\EventDispatcher\Tester;
  18  
  19  use Behat\Behat\EventDispatcher\Event\AfterStepSetup;
  20  use Behat\Behat\EventDispatcher\Event\AfterStepTested;
  21  use Behat\Behat\EventDispatcher\Event\BeforeStepTeardown;
  22  use Behat\Behat\EventDispatcher\Event\BeforeStepTested;
  23  use Behat\Behat\Tester\Result\ExecutedStepResult;
  24  use Behat\Behat\Tester\Result\SkippedStepResult;
  25  use Behat\Behat\Tester\Result\StepResult;
  26  use Behat\Behat\Tester\Result\UndefinedStepResult;
  27  use Behat\Behat\Tester\StepTester;
  28  use Behat\Gherkin\Node\FeatureNode;
  29  use Behat\Gherkin\Node\StepNode;
  30  use Behat\Testwork\Call\CallResult;
  31  use Behat\Testwork\Environment\Environment;
  32  use Behat\Testwork\EventDispatcher\TestworkEventDispatcher;
  33  use Moodle\BehatExtension\Context\Step\ChainedStep;
  34  use Moodle\BehatExtension\Exception\SkippedException;
  35  use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  36  
  37  // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
  38  
  39  /**
  40   * Override step tester to ensure chained steps gets executed.
  41   *
  42   * @package    core
  43   * @copyright  2016 Rajesh Taneja <rajesh@moodle.com>
  44   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class ChainedStepTester implements StepTester {
  47      /**
  48       * The text of the step to look for exceptions / debugging messages.
  49       */
  50      const EXCEPTIONS_STEP_TEXT = 'I look for exceptions';
  51  
  52      /**
  53       * @var StepTester Base step tester.
  54       */
  55      private $singlesteptester;
  56  
  57      /**
  58       * @var EventDispatcher keep step event dispatcher.
  59       */
  60      private $eventdispatcher;
  61  
  62      /**
  63       * Keep status of chained steps if used.
  64       * @var bool
  65       */
  66      protected static $chainedstepused = false;
  67  
  68      /**
  69       * Constructor.
  70       *
  71       * @param StepTester $steptester single step tester.
  72       */
  73      public function __construct(StepTester $steptester) {
  74          $this->singlesteptester = $steptester;
  75      }
  76  
  77      /**
  78       * Set event dispatcher to use for events.
  79       *
  80       * @param EventDispatcherInterface $eventdispatcher
  81       */
  82      public function setEventDispatcher(EventDispatcherInterface $eventdispatcher) {
  83          $this->eventdispatcher = $eventdispatcher;
  84      }
  85  
  86      /**
  87       * Sets up step for a test.
  88       *
  89       * @param Environment $env
  90       * @param FeatureNode $feature
  91       * @param StepNode    $step
  92       * @param bool     $skip
  93       *
  94       * @return Setup
  95       */
  96      public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
  97          return $this->singlesteptester->setUp($env, $feature, $step, $skip);
  98      }
  99  
 100      /**
 101       * Tests step.
 102       *
 103       * @param Environment $env
 104       * @param FeatureNode $feature
 105       * @param StepNode    $step
 106       * @param bool     $skip
 107       * @return StepResult
 108       */
 109      public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip) {
 110          $result = $this->singlesteptester->test($env, $feature, $step, $skip);
 111  
 112          if (!($result instanceof ExecutedStepResult) || !$this->supportsResult($result->getCallResult())) {
 113              $result = $this->checkSkipResult($result);
 114  
 115              // If undefined step then don't continue chained steps.
 116              if ($result instanceof UndefinedStepResult) {
 117                  return $result;
 118              }
 119  
 120              // If exception caught, then don't continue chained steps.
 121              if (($result instanceof ExecutedStepResult) && $result->hasException()) {
 122                  return $result;
 123              }
 124  
 125              // If step is skipped, then return. no need to continue chain steps.
 126              if ($result instanceof SkippedStepResult) {
 127                  return $result;
 128              }
 129  
 130              // Check for exceptions.
 131              // Extra step, looking for a moodle exception, a debugging() message or a PHP debug message.
 132              $checkingstep = new StepNode('Given', self::EXCEPTIONS_STEP_TEXT, [], $step->getLine());
 133              $afterexceptioncheckingevent = $this->singlesteptester->test($env, $feature, $checkingstep, $skip);
 134              $exceptioncheckresult = $this->checkSkipResult($afterexceptioncheckingevent);
 135  
 136              if (!$exceptioncheckresult->isPassed()) {
 137                  return $exceptioncheckresult;
 138              }
 139  
 140              return $result;
 141          }
 142  
 143          return $this->runChainedSteps($env, $feature, $result, $skip);
 144      }
 145  
 146      /**
 147       * Tears down step after a test.
 148       *
 149       * @param Environment $env
 150       * @param FeatureNode $feature
 151       * @param StepNode    $step
 152       * @param bool     $skip
 153       * @param StepResult  $result
 154       * @return Teardown
 155       */
 156      public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result) {
 157          return $this->singlesteptester->tearDown($env, $feature, $step, $skip, $result);
 158      }
 159  
 160      /**
 161       * Check if results supported.
 162       *
 163       * @param CallResult $result
 164       * @return bool
 165       */
 166      private function supportsResult(CallResult $result) {
 167          $return = $result->getReturn();
 168          if ($return instanceof ChainedStep) {
 169              return true;
 170          }
 171          if (!is_array($return) || empty($return)) {
 172              return false;
 173          }
 174          foreach ($return as $value) {
 175              if (!$value instanceof ChainedStep) {
 176                  return false;
 177              }
 178          }
 179          return true;
 180      }
 181  
 182      /**
 183       * Run chained steps.
 184       *
 185       * @param Environment $env
 186       * @param FeatureNode $feature
 187       * @param ExecutedStepResult $result
 188       * @param bool $skip
 189       * @return ExecutedStepResult|StepResult
 190       */
 191      private function runChainedSteps(Environment $env, FeatureNode $feature, ExecutedStepResult $result, $skip) {
 192          // Set chained setp is used, so it can be used by formatter to o/p.
 193          self::$chainedstepused = true;
 194  
 195          $callresult = $result->getCallResult();
 196          $steps = $callresult->getReturn();
 197  
 198          if (!is_array($steps)) {
 199              // Test it, no need to dispatch events for single chain.
 200              $stepresult = $this->test($env, $feature, $steps, $skip);
 201              return $this->checkSkipResult($stepresult);
 202          }
 203  
 204          // Test all steps.
 205          foreach ($steps as $step) {
 206              // Setup new step.
 207              $event = new BeforeStepTested($env, $feature, $step);
 208              if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
 209                  // Symfony 4.3 and up.
 210                  $this->eventdispatcher->dispatch($event, $event::BEFORE);
 211              } else {
 212                  // TODO: Remove when our min supported version is >= 4.3.
 213                  $this->eventdispatcher->dispatch($event::BEFORE, $event);
 214              }
 215  
 216              $setup = $this->setUp($env, $feature, $step, $skip);
 217  
 218              $event = new AfterStepSetup($env, $feature, $step, $setup);
 219              if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
 220                  // Symfony 4.3 and up.
 221                  $this->eventdispatcher->dispatch($event, $event::AFTER_SETUP);
 222              } else {
 223                  // TODO: Remove when our min supported version is >= 4.3.
 224                  $this->eventdispatcher->dispatch($event::AFTER_SETUP, $event);
 225              }
 226  
 227              // Test it.
 228              $stepresult = $this->test($env, $feature, $step, $skip);
 229  
 230              // Tear down.
 231              $event = new BeforeStepTeardown($env, $feature, $step, $result);
 232              if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
 233                  // Symfony 4.3 and up.
 234                  $this->eventdispatcher->dispatch($event, $event::BEFORE_TEARDOWN);
 235              } else {
 236                  // TODO: Remove when our min supported version is >= 4.3.
 237                  $this->eventdispatcher->dispatch($event::BEFORE_TEARDOWN, $event);
 238              }
 239  
 240              $teardown = $this->tearDown($env, $feature, $step, $skip, $result);
 241  
 242              $event = new AfterStepTested($env, $feature, $step, $result, $teardown);
 243              if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) {
 244                  // Symfony 4.3 and up.
 245                  $this->eventdispatcher->dispatch($event, $event::AFTER);
 246              } else {
 247                  // TODO: Remove when our min supported version is >= 4.3.
 248                  $this->eventdispatcher->dispatch($event::AFTER, $event);
 249              }
 250  
 251              if (!$stepresult->isPassed()) {
 252                  return $this->checkSkipResult($stepresult);
 253              }
 254          }
 255          return $this->checkSkipResult($stepresult);
 256      }
 257  
 258      /**
 259       * Handle skip exception.
 260       *
 261       * @param StepResult $result
 262       *
 263       * @return ExecutedStepResult|SkippedStepResult
 264       */
 265      private function checkSkipResult(StepResult $result) {
 266          if ((method_exists($result, 'getException')) && ($result->getException() instanceof SkippedException)) {
 267              return new SkippedStepResult($result->getSearchResult());
 268          } else {
 269              return $result;
 270          }
 271      }
 272  
 273      /**
 274       * Returns if cahined steps are used.
 275       * @return bool.
 276       */
 277      public static function is_chained_step_used() {
 278          return self::$chainedstepused;
 279      }
 280  }