Step.php 9.5 KB

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