RuntimeStepTester   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 49
dl 0
loc 202
rs 10
c 0
b 0
f 0
wmc 22

10 Methods

Rating   Name   Duplication   Size   Complexity  
A updateLastKeyword() 0 5 2
A tearDown() 0 3 1
A createDefinitionCall() 0 6 1
A searchDefinition() 0 9 4
A testDefinition() 0 15 3
A setUp() 0 3 1
A __construct() 0 4 1
A makeCall() 0 12 3
A spin() 0 17 4
A test() 0 12 2
1
<?php namespace Chekote\BehatRetryExtension\Tester;
2
3
use Behat\Behat\Definition\Call\DefinitionCall;
4
use Behat\Behat\Definition\DefinitionFinder;
5
use Behat\Behat\Definition\Exception\SearchException;
6
use Behat\Behat\Definition\SearchResult;
7
use Behat\Behat\Tester\Exception\PendingException;
8
use Behat\Behat\Tester\Result\ExecutedStepResult;
9
use Behat\Behat\Tester\Result\FailedStepSearchResult;
10
use Behat\Behat\Tester\Result\SkippedStepResult;
11
use Behat\Behat\Tester\Result\StepResult;
12
use Behat\Behat\Tester\Result\UndefinedStepResult;
13
use Behat\Behat\Tester\StepTester;
14
use Behat\Gherkin\Node\FeatureNode;
15
use Behat\Gherkin\Node\StepNode;
16
use Behat\Testwork\Call\CallCenter;
17
use Behat\Testwork\Call\CallResult;
18
use Behat\Testwork\Environment\Environment;
19
use Behat\Testwork\Tester\Setup\SuccessfulSetup;
20
use Behat\Testwork\Tester\Setup\SuccessfulTeardown;
21
use Chekote\BehatRetryExtension\Definition\Exception\StrictKeywordException;
22
23
/**
24
 * Tester executing step tests in the runtime.
25
 *
26
 * This class is a copy of \Behat\Behat\Tester\Runtime\RuntimeStepTester by Konstantin Kudryashov <[email protected]>.
27
 * It has a modified testDefinition() method to implement the retry functionality, and support strict keywords.
28
 *
29
 * I would ideally like to extend or wrap the existing RuntimeStepTester, but neither is possible because the class
30
 * is final, and the method is private. v_v
31
 */
32
final class RuntimeStepTester implements StepTester
33
{
34
    /** @var float Number of seconds to attempt "Then" steps before accepting a failure */
35
    public static $timeout;
36
37
    /** @var int number of nanoseconds to wait between each retry of "Then" steps */
38
    public static $interval;
39
40
    /**
41
     * @var bool true if steps should only match when the correct keyword is used. false steps should match regardless
42
     *           of keyword
43
     */
44
    public static $strictKeywords;
45
46
    /** @var array list of Gherkin keywords */
47
    protected static $keywords = ['Given', 'When', 'Then'];
48
49
    /**
50
     * @var DefinitionFinder
51
     */
52
    private $definitionFinder;
53
54
    /**
55
     * @var CallCenter
56
     */
57
    private $callCenter;
58
59
    /** @var string The last "Given", "When", or "Then" keyword encountered */
60
    protected $lastKeyword;
61
62
    /**
63
     * Initialize tester.
64
     *
65
     * @param DefinitionFinder $definitionFinder
66
     * @param CallCenter       $callCenter
67
     */
68
    public function __construct(DefinitionFinder $definitionFinder, CallCenter $callCenter)
69
    {
70
        $this->definitionFinder = $definitionFinder;
71
        $this->callCenter = $callCenter;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip)
78
    {
79
        return new SuccessfulSetup();
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip = false)
86
    {
87
        $this->updateLastKeyword($step);
88
89
        try {
90
            $search = $this->searchDefinition($env, $feature, $step);
91
            $result = $this->testDefinition($env, $feature, $step, $search, $skip);
92
        } catch (SearchException $exception) {
93
            $result = new FailedStepSearchResult($exception);
94
        }
95
96
        return $result;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result)
103
    {
104
        return new SuccessfulTeardown();
105
    }
106
107
    /**
108
     * Searches for a definition.
109
     *
110
     * @param Environment $env
111
     * @param FeatureNode $feature
112
     * @param StepNode    $step
113
     *
114
     * @return SearchResult
115
     */
116
    private function searchDefinition(Environment $env, FeatureNode $feature, StepNode $step)
117
    {
118
        $result = $this->definitionFinder->findDefinition($env, $feature, $step);
119
120
        if (self::$strictKeywords && $result->hasMatch() && $this->lastKeyword != $result->getMatchedDefinition()->getType()) {
121
            throw new StrictKeywordException($this->lastKeyword, $result->getMatchedDefinition());
122
        }
123
124
        return $result;
125
    }
126
127
    /**
128
     * Tests found definition.
129
     *
130
     * @param Environment  $env
131
     * @param FeatureNode  $feature
132
     * @param StepNode     $step
133
     * @param SearchResult $search
134
     * @param bool         $skip
135
     *
136
     * @return StepResult
137
     */
138
    private function testDefinition(Environment $env, FeatureNode $feature, StepNode $step, SearchResult $search, $skip)
139
    {
140
        if (!$search->hasMatch()) {
141
            return new UndefinedStepResult();
142
        }
143
144
        if ($skip) {
145
            return new SkippedStepResult($search);
146
        }
147
148
        $call = $this->createDefinitionCall($env, $feature, $search, $step);
149
150
        $result = $this->makeCall($call);
151
152
        return new ExecutedStepResult($search, $result);
153
    }
154
155
    /**
156
     * Records the keyword for the step.
157
     *
158
     * This allows us to know where we are when processing And or But steps.
159
     *
160
     * @param  StepNode $step
161
     * @return void
162
     */
163
    protected function updateLastKeyword(StepNode $step)
164
    {
165
        $keyword = $step->getKeyword();
166
        if (in_array($keyword, self::$keywords)) {
167
            $this->lastKeyword = $keyword;
168
        }
169
    }
170
171
    /**
172
     * Calls the specified definition, either directly, or via spin() if self::$timeout is not 0.
173
     *
174
     * @param  DefinitionCall $call the call to make.
175
     * @return CallResult     the result of the call.
176
     */
177
    protected function makeCall(DefinitionCall $call)
178
    {
179
        // @todo We can only "spin" if we are interacting with a remote browser. If the browser is
180
        // running in the same thread as this test (such as with Goutte or Zombie), then spinning
181
        // will only prevent that process from continuing, and the test will either pass immediately,
182
        // or not at all. We need to find out how to check what Driver we're using...
183
        if ($this->lastKeyword == 'Then' && self::$timeout) {
184
            return $this->spin(function () use ($call) {
185
                return $this->callCenter->makeCall($call);
186
            });
187
        } else {
188
            return $this->callCenter->makeCall($call);
189
        }
190
    }
191
192
    /**
193
     * Continually calls an assertion until it passes or the timeout is reached.
194
     *
195
     * @param  callable   $lambda The lambda assertion to call. Must take no arguments and return
196
     *                            a CallResult.
197
     * @return CallResult
198
     */
199
    protected function spin(callable $lambda)
200
    {
201
        $start = microtime(true);
202
203
        $result = null;
204
        while (microtime(true) - $start < self::$timeout) {
205
            /** @var $result CallResult */
206
            $result = $lambda();
207
208
            if (!$result->hasException() || ($result->getException() instanceof PendingException)) {
209
                break;
210
            }
211
212
            time_nanosleep(0, self::$interval);
213
        }
214
215
        return $result;
216
    }
217
218
    /**
219
     * Creates definition call.
220
     *
221
     * @param Environment  $env
222
     * @param FeatureNode  $feature
223
     * @param SearchResult $search
224
     * @param StepNode     $step
225
     *
226
     * @return DefinitionCall
227
     */
228
    private function createDefinitionCall(Environment $env, FeatureNode $feature, SearchResult $search, StepNode $step)
229
    {
230
        $definition = $search->getMatchedDefinition();
231
        $arguments = $search->getMatchedArguments();
232
233
        return new DefinitionCall($env, $feature, $step, $definition, $arguments);
0 ignored issues
show
Bug introduced by
It seems like $arguments can also be of type null; however, parameter $arguments of Behat\Behat\Definition\C...tionCall::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
        return new DefinitionCall($env, $feature, $step, $definition, /** @scrutinizer ignore-type */ $arguments);
Loading history...
Bug introduced by
It seems like $definition can also be of type null; however, parameter $definition of Behat\Behat\Definition\C...tionCall::__construct() does only seem to accept Behat\Behat\Definition\Definition, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
        return new DefinitionCall($env, $feature, $step, /** @scrutinizer ignore-type */ $definition, $arguments);
Loading history...
234
    }
235
}
236