Step   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 323
rs 3.52
c 0
b 0
f 0
wmc 61
lcom 1
cbo 4

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A saveTrace() 0 18 3
A isTestFile() 0 4 1
A getName() 0 5 1
A getAction() 0 4 1
A getLine() 0 6 3
A hasFailed() 0 4 1
A getArguments() 0 4 1
B getArgumentsAsString() 0 48 9
B stringifyArgument() 0 27 10
A getClassName() 0 10 3
A formatClassName() 0 4 1
A getPhpCode() 0 8 1
A getMetaStep() 0 4 1
A __toString() 0 5 1
A toString() 0 6 1
A getHtml() 0 8 2
A getHumanizedActionWithoutArguments() 0 4 1
A getHumanizedArguments() 0 4 1
A clean() 0 4 1
A humanize() 0 7 1
A run() 0 23 5
B addMetaStep() 0 33 9
A setMetaStep() 0 4 1
A getPrefix() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Step often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Step, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Codeception;
3
4
use Codeception\Lib\ModuleContainer;
5
use Codeception\Step\Argument\FormattedOutput;
6
use Codeception\Step\Meta as MetaStep;
7
use Codeception\Util\Locator;
8
9
abstract class Step
10
{
11
    const STACK_POSITION = 3;
12
    /**
13
     * @var    string
14
     */
15
    protected $action;
16
17
    /**
18
     * @var    array
19
     */
20
    protected $arguments;
21
22
    protected $debugOutput;
23
24
    public $executed = false;
25
26
    protected $line = null;
27
    protected $file = null;
28
    protected $prefix = 'I';
29
30
    /**
31
     * @var MetaStep
32
     */
33
    protected $metaStep = null;
34
35
    protected $failed = false;
36
37
    public function __construct($action, array $arguments = [])
38
    {
39
        $this->action = $action;
40
        $this->arguments = $arguments;
41
    }
42
43
    public function saveTrace()
44
    {
45
        $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
46
47
        if (count($stack) <= self::STACK_POSITION) {
48
            return;
49
        }
50
51
        $traceLine = $stack[self::STACK_POSITION - 1];
52
53
        if (!isset($traceLine['file'])) {
54
            return;
55
        }
56
        $this->file = $traceLine['file'];
57
        $this->line = $traceLine['line'];
58
59
        $this->addMetaStep($traceLine, $stack);
60
    }
61
62
    private function isTestFile($file)
63
    {
64
        return preg_match('~[^\\'.DIRECTORY_SEPARATOR.'](Cest|Cept|Test).php$~', $file);
65
    }
66
67
    public function getName()
68
    {
69
        $class = explode('\\', __CLASS__);
70
        return end($class);
71
    }
72
73
    public function getAction()
74
    {
75
        return $this->action;
76
    }
77
78
    public function getLine()
79
    {
80
        if ($this->line && $this->file) {
81
            return codecept_relative_path($this->file) . ':' . $this->line;
82
        }
83
    }
84
85
    public function hasFailed()
86
    {
87
        return $this->failed;
88
    }
89
90
    public function getArguments()
91
    {
92
        return $this->arguments;
93
    }
94
95
    public function getArgumentsAsString($maxLength = 200)
96
    {
97
        $arguments = $this->arguments;
98
99
        $argumentCount = count($arguments);
100
        $totalLength = $argumentCount - 1; // count separators before adding length of individual arguments
101
102
        foreach ($arguments as $key => $argument) {
103
            $stringifiedArgument = $this->stringifyArgument($argument);
104
            $arguments[$key] = $stringifiedArgument;
105
            $totalLength += mb_strlen($stringifiedArgument, 'utf-8');
106
        }
107
108
        if ($totalLength > $maxLength && $maxLength > 0) {
109
            //sort arguments from shortest to longest
110
            uasort($arguments, function ($arg1, $arg2) {
111
                $length1 = mb_strlen($arg1, 'utf-8');
112
                $length2 = mb_strlen($arg2, 'utf-8');
113
                if ($length1 === $length2) {
114
                    return 0;
115
                }
116
                return ($length1 < $length2) ? -1 : 1;
117
            });
118
119
            $allowedLength = floor(($maxLength - $argumentCount + 1) / $argumentCount);
120
121
            $lengthRemaining = $maxLength;
122
            $argumentsRemaining = $argumentCount;
123
            foreach ($arguments as $key => $argument) {
124
                $argumentsRemaining--;
125
                if (mb_strlen($argument, 'utf-8') > $allowedLength) {
126
                    $arguments[$key] = mb_substr($argument, 0, $allowedLength - 4, 'utf-8') . '...' . mb_substr($argument, -1, 1, 'utf-8');
127
                    $lengthRemaining -= ($allowedLength + 1);
128
                } else {
129
                    $lengthRemaining -= (mb_strlen($arguments[$key], 'utf-8') + 1);
130
                    //recalculate allowed length because this argument was short
131
                    if ($argumentsRemaining > 0) {
132
                        $allowedLength = floor(($lengthRemaining - $argumentsRemaining + 1) / $argumentsRemaining);
133
                    }
134
                }
135
            }
136
137
            //restore original order of arguments
138
            ksort($arguments);
139
        }
140
141
        return implode(',', $arguments);
142
    }
143
144
    protected function stringifyArgument($argument)
145
    {
146
        if (is_string($argument)) {
147
            return '"' . strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']) . '"';
148
        } elseif (is_resource($argument)) {
149
            $argument = (string)$argument;
150
        } elseif (is_array($argument)) {
151
            foreach ($argument as $key => $value) {
152
                if (is_object($value)) {
153
                    $argument[$key] = $this->getClassName($value);
154
                }
155
            }
156
        } elseif (is_object($argument)) {
157
            if ($argument instanceof FormattedOutput) {
158
                $argument = $argument->getOutput();
159
            } elseif (method_exists($argument, '__toString')) {
160
                $argument = (string)$argument;
161
            } elseif (get_class($argument) == 'Facebook\WebDriver\WebDriverBy') {
162
                $argument = Locator::humanReadableString($argument);
163
            } else {
164
                $argument = $this->getClassName($argument);
165
            }
166
        }
167
        $arg_str = json_encode($argument, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
168
        $arg_str = str_replace('\"', '"', $arg_str);
169
        return $arg_str;
170
    }
171
172
    protected function getClassName($argument)
173
    {
174
        if ($argument instanceof \Closure) {
175
            return 'Closure';
176
        } elseif ((isset($argument->__mocked))) {
177
            return $this->formatClassName($argument->__mocked);
178
        }
179
180
        return $this->formatClassName(get_class($argument));
181
    }
182
183
    protected function formatClassName($classname)
184
    {
185
        return trim($classname, "\\");
186
    }
187
188
    public function getPhpCode($maxLength)
189
    {
190
        $result = "\${$this->prefix}->" . $this->getAction() . '(';
191
        $maxLength = $maxLength - mb_strlen($result, 'utf-8') - 1;
192
193
        $result .= $this->getHumanizedArguments($maxLength) .')';
194
        return $result;
195
    }
196
197
    /**
198
     * @return MetaStep
199
     */
200
    public function getMetaStep()
201
    {
202
        return $this->metaStep;
203
    }
204
205
    public function __toString()
206
    {
207
        $humanizedAction = $this->humanize($this->getAction());
208
        return $humanizedAction . ' ' . $this->getHumanizedArguments();
209
    }
210
211
212
    public function toString($maxLength)
213
    {
214
        $humanizedAction = $this->humanize($this->getAction());
215
        $maxLength = $maxLength - mb_strlen($humanizedAction, 'utf-8') - 1;
216
        return $humanizedAction . ' ' . $this->getHumanizedArguments($maxLength);
217
    }
218
219
    public function getHtml($highlightColor = '#732E81')
220
    {
221
        if (empty($this->arguments)) {
222
            return sprintf('%s %s', ucfirst($this->prefix), $this->humanize($this->getAction()));
223
        }
224
225
        return sprintf('%s %s <span style="color: %s">%s</span>', ucfirst($this->prefix), htmlspecialchars($this->humanize($this->getAction())), $highlightColor, htmlspecialchars($this->getHumanizedArguments()));
226
    }
227
228
    public function getHumanizedActionWithoutArguments()
229
    {
230
        return $this->humanize($this->getAction());
231
    }
232
233
    public function getHumanizedArguments($maxLength = 200)
234
    {
235
        return $this->getArgumentsAsString($maxLength);
236
    }
237
238
    protected function clean($text)
239
    {
240
        return str_replace('\/', '', $text);
241
    }
242
243
    protected function humanize($text)
244
    {
245
        $text = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1 \\2', $text);
246
        $text = preg_replace('/([a-z\d])([A-Z])/', '\\1 \\2', $text);
247
        $text = preg_replace('~\bdont\b~', 'don\'t', $text);
248
        return mb_strtolower($text, 'UTF-8');
249
    }
250
251
    public function run(ModuleContainer $container = null)
252
    {
253
        $this->executed = true;
254
        if (!$container) {
255
            return null;
256
        }
257
        $activeModule = $container->moduleForAction($this->action);
258
259
        if (!is_callable([$activeModule, $this->action])) {
260
            throw new \RuntimeException("Action '{$this->action}' can't be called");
261
        }
262
263
        try {
264
            $res = call_user_func_array([$activeModule, $this->action], $this->arguments);
265
        } catch (\Exception $e) {
266
            $this->failed = true;
267
            if ($this->getMetaStep()) {
268
                $this->getMetaStep()->setFailed(true);
269
            }
270
            throw $e;
271
        }
272
        return $res;
273
    }
274
275
    /**
276
     * If steps are combined into one method they can be reproduced as meta-step.
277
     * We are using stack trace to analyze if steps were called from test, if not - they were called from meta-step.
278
     *
279
     * @param $step
280
     * @param $stack
281
     */
282
    protected function addMetaStep($step, $stack)
283
    {
284
        if (($this->isTestFile($this->file)) || ($step['class'] == 'Codeception\Scenario')) {
285
            return;
286
        }
287
288
        $i = count($stack) - self::STACK_POSITION - 1;
289
290
        // get into test file and retrieve its actual call
291
        while (isset($stack[$i])) {
292
            $step = $stack[$i];
293
            $i--;
294
            if (!isset($step['file']) or !isset($step['function']) or !isset($step['class'])) {
295
                continue;
296
            }
297
298
            if (!$this->isTestFile($step['file'])) {
299
                continue;
300
            }
301
302
            // in case arguments were passed by reference, copy args array to ensure dereference.  array_values() does not dereference values
303
            $this->metaStep = new Step\Meta($step['function'], array_map(function ($i) {
304
                return $i;
305
            }, array_values($step['args'])));
306
            $this->metaStep->setTraceInfo($step['file'], $step['line']);
307
308
            // pageobjects or other classes should not be included with "I"
309
            if (!in_array('Codeception\Actor', class_parents($step['class']))) {
310
                $this->metaStep->setPrefix($step['class'] . ':');
311
            }
312
            return;
313
        }
314
    }
315
316
    /**
317
     * @param MetaStep $metaStep
318
     */
319
    public function setMetaStep($metaStep)
320
    {
321
        $this->metaStep = $metaStep;
322
    }
323
324
    /**
325
     * @return string
326
     */
327
    public function getPrefix()
328
    {
329
        return $this->prefix . ' ';
330
    }
331
}
332