Passed
Branch master (46933e)
by Donald
01:26
created

RuntimeStepTester::makeCall()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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