Configuration.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. <?php
  2. namespace Codeception;
  3. use Codeception\Exception\ConfigurationException;
  4. use Codeception\Lib\ParamsLoader;
  5. use Codeception\Util\Autoload;
  6. use Codeception\Util\Template;
  7. use Symfony\Component\Finder\Finder;
  8. use Symfony\Component\Finder\SplFileInfo;
  9. use Symfony\Component\Yaml\Exception\ParseException;
  10. use Symfony\Component\Yaml\Yaml;
  11. class Configuration
  12. {
  13. protected static $suites = [];
  14. /**
  15. * @var array Current configuration
  16. */
  17. protected static $config = null;
  18. /**
  19. * @var array environmental files configuration cache
  20. */
  21. protected static $envConfig = [];
  22. /**
  23. * @var string Directory containing main configuration file.
  24. * @see self::projectDir()
  25. */
  26. protected static $dir = null;
  27. /**
  28. * @var string Current project logs directory.
  29. */
  30. protected static $outputDir = null;
  31. /**
  32. * @var string Current project data directory. This directory is used to hold
  33. * sql dumps and other things needed for current project tests.
  34. */
  35. protected static $dataDir = null;
  36. /**
  37. * @var string Directory with test support files like Actors, Helpers, PageObjects, etc
  38. */
  39. protected static $supportDir = null;
  40. /**
  41. * @var string Directory containing environment configuration files.
  42. */
  43. protected static $envsDir = null;
  44. /**
  45. * @var string Directory containing tests and suites of the current project.
  46. */
  47. protected static $testsDir = null;
  48. public static $lock = false;
  49. protected static $di;
  50. /**
  51. * @var array Default config
  52. */
  53. public static $defaultConfig = [
  54. 'actor_suffix'=> 'Tester',
  55. 'namespace' => '',
  56. 'include' => [],
  57. 'paths' => [],
  58. 'suites' => [],
  59. 'modules' => [],
  60. 'extensions' => [
  61. 'enabled' => [],
  62. 'config' => [],
  63. 'commands' => [],
  64. ],
  65. 'reporters' => [
  66. 'xml' => 'Codeception\PHPUnit\Log\JUnit',
  67. 'html' => 'Codeception\PHPUnit\ResultPrinter\HTML',
  68. 'tap' => 'PHPUnit_Util_Log_TAP',
  69. 'json' => 'PHPUnit_Util_Log_JSON',
  70. 'report' => 'Codeception\PHPUnit\ResultPrinter\Report',
  71. ],
  72. 'groups' => [],
  73. 'settings' => [
  74. 'colors' => true,
  75. 'bootstrap' => false,
  76. 'strict_xml' => false,
  77. 'lint' => true,
  78. 'backup_globals' => true,
  79. 'log_incomplete_skipped' => false,
  80. 'report_useless_tests' => false,
  81. 'disallow_test_output' => false,
  82. 'be_strict_about_changes_to_global_state' => false
  83. ],
  84. 'coverage' => [],
  85. 'params' => [],
  86. 'gherkin' => []
  87. ];
  88. public static $defaultSuiteSettings = [
  89. 'actor' => null,
  90. 'class_name' => null, // Codeception <2.3 compatibility
  91. 'modules' => [
  92. 'enabled' => [],
  93. 'config' => [],
  94. 'depends' => []
  95. ],
  96. 'path' => null,
  97. 'namespace' => null,
  98. 'groups' => [],
  99. 'shuffle' => false,
  100. 'extensions' => [ // suite extensions
  101. 'enabled' => [],
  102. 'config' => [],
  103. ],
  104. 'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED',
  105. ];
  106. protected static $params;
  107. /**
  108. * Loads global config file which is `codeception.yml` by default.
  109. * When config is already loaded - returns it.
  110. *
  111. * @param null $configFile
  112. * @return array
  113. * @throws Exception\ConfigurationException
  114. */
  115. public static function config($configFile = null)
  116. {
  117. if (!$configFile && self::$config) {
  118. return self::$config;
  119. }
  120. if (self::$config && self::$lock) {
  121. return self::$config;
  122. }
  123. if ($configFile === null) {
  124. $configFile = getcwd() . DIRECTORY_SEPARATOR . 'codeception.yml';
  125. }
  126. if (is_dir($configFile)) {
  127. $configFile = $configFile . DIRECTORY_SEPARATOR . 'codeception.yml';
  128. }
  129. $dir = realpath(dirname($configFile));
  130. self::$dir = $dir;
  131. $configDistFile = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml';
  132. if (!(file_exists($configDistFile) || file_exists($configFile))) {
  133. throw new ConfigurationException("Configuration file could not be found.\nRun `bootstrap` to initialize Codeception.", 404);
  134. }
  135. // Preload config to retrieve params such that they are applied to codeception config file below
  136. $tempConfig = self::$defaultConfig;
  137. $distConfigContents = "";
  138. if (file_exists($configDistFile)) {
  139. $distConfigContents = file_get_contents($configDistFile);
  140. $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($distConfigContents, $configDistFile));
  141. }
  142. $configContents = "";
  143. if (file_exists($configFile)) {
  144. $configContents = file_get_contents($configFile);
  145. $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($configContents, $configFile));
  146. }
  147. self::prepareParams($tempConfig);
  148. // load config using params
  149. $config = self::mergeConfigs(self::$defaultConfig, self::getConfFromContents($distConfigContents, $configDistFile));
  150. $config = self::mergeConfigs($config, self::getConfFromContents($configContents, $configFile));
  151. if ($config == self::$defaultConfig) {
  152. throw new ConfigurationException("Configuration file is invalid");
  153. }
  154. self::$config = $config;
  155. // compatibility with 1.x, 2.0
  156. if (!isset($config['paths']['output']) and isset($config['paths']['log'])) {
  157. $config['paths']['output'] = $config['paths']['log'];
  158. }
  159. if (isset(self::$config['actor'])) {
  160. self::$config['actor_suffix'] = self::$config['actor']; // old compatibility
  161. }
  162. if (!isset($config['paths']['support']) and isset($config['paths']['helpers'])) {
  163. $config['paths']['support'] = $config['paths']['helpers'];
  164. }
  165. if (!isset($config['paths']['output'])) {
  166. throw new ConfigurationException('Output path is not defined by key "paths: output"');
  167. }
  168. self::$outputDir = $config['paths']['output'];
  169. // fill up includes with wildcard expansions
  170. $config['include'] = self::expandWildcardedIncludes($config['include']);
  171. // config without tests, for inclusion of other configs
  172. if (count($config['include'])) {
  173. self::$config = $config;
  174. if (!isset($config['paths']['tests'])) {
  175. return $config;
  176. }
  177. }
  178. if (!isset($config['paths']['tests'])) {
  179. throw new ConfigurationException(
  180. 'Tests directory is not defined in Codeception config by key "paths: tests:"'
  181. );
  182. }
  183. if (!isset($config['paths']['data'])) {
  184. throw new ConfigurationException('Data path is not defined Codeception config by key "paths: data"');
  185. }
  186. if (!isset($config['paths']['support'])) {
  187. throw new ConfigurationException('Helpers path is not defined by key "paths: support"');
  188. }
  189. self::$dataDir = $config['paths']['data'];
  190. self::$supportDir = $config['paths']['support'];
  191. self::$testsDir = $config['paths']['tests'];
  192. if (isset($config['paths']['envs'])) {
  193. self::$envsDir = $config['paths']['envs'];
  194. }
  195. Autoload::addNamespace(self::$config['namespace'], self::supportDir());
  196. self::loadBootstrap($config['settings']['bootstrap']);
  197. self::loadSuites();
  198. return $config;
  199. }
  200. protected static function loadBootstrap($bootstrap)
  201. {
  202. if (!$bootstrap) {
  203. return;
  204. }
  205. $bootstrap = self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR . $bootstrap;
  206. if (file_exists($bootstrap)) {
  207. include_once $bootstrap;
  208. }
  209. }
  210. protected static function loadSuites()
  211. {
  212. $suites = Finder::create()
  213. ->files()
  214. ->name('*.{suite,suite.dist}.yml')
  215. ->in(self::$dir . DIRECTORY_SEPARATOR . self::$testsDir)
  216. ->depth('< 1')
  217. ->sortByName();
  218. self::$suites = [];
  219. foreach (array_keys(self::$config['suites']) as $suite) {
  220. self::$suites[$suite] = $suite;
  221. }
  222. /** @var SplFileInfo $suite */
  223. foreach ($suites as $suite) {
  224. preg_match('~(.*?)(\.suite|\.suite\.dist)\.yml~', $suite->getFilename(), $matches);
  225. self::$suites[$matches[1]] = $matches[1];
  226. }
  227. }
  228. /**
  229. * Returns suite configuration. Requires suite name and global config used (Configuration::config)
  230. *
  231. * @param string $suite
  232. * @param array $config
  233. * @return array
  234. * @throws \Exception
  235. */
  236. public static function suiteSettings($suite, $config)
  237. {
  238. // cut namespace name from suite name
  239. if ($suite != $config['namespace'] && substr($suite, 0, strlen($config['namespace'])) == $config['namespace']) {
  240. $suite = substr($suite, strlen($config['namespace']));
  241. }
  242. if (!in_array($suite, self::$suites)) {
  243. throw new ConfigurationException("Suite $suite was not loaded");
  244. }
  245. // load global config
  246. $globalConf = $config['settings'];
  247. foreach (['modules', 'coverage', 'namespace', 'groups', 'env', 'gherkin', 'extensions'] as $key) {
  248. if (isset($config[$key])) {
  249. $globalConf[$key] = $config[$key];
  250. }
  251. }
  252. $settings = self::mergeConfigs(self::$defaultSuiteSettings, $globalConf);
  253. // load suite config
  254. $settings = self::loadSuiteConfig($suite, $config['paths']['tests'], $settings);
  255. // load from environment configs
  256. if (isset($config['paths']['envs'])) {
  257. $envConf = self::loadEnvConfigs(self::$dir . DIRECTORY_SEPARATOR . $config['paths']['envs']);
  258. $settings = self::mergeConfigs($settings, $envConf);
  259. }
  260. if (!$settings['actor']) {
  261. // Codeception 2.2 compatibility
  262. $settings['actor'] = $settings['class_name'];
  263. }
  264. if (!$settings['path']) {
  265. // take a suite path from its name
  266. $settings['path'] = $suite;
  267. }
  268. $settings['path'] = self::$dir . DIRECTORY_SEPARATOR . $config['paths']['tests']
  269. . DIRECTORY_SEPARATOR . $settings['path'] . DIRECTORY_SEPARATOR;
  270. return $settings;
  271. }
  272. /**
  273. * Loads environments configuration from set directory
  274. *
  275. * @param string $path path to the directory
  276. * @return array
  277. */
  278. protected static function loadEnvConfigs($path)
  279. {
  280. if (isset(self::$envConfig[$path])) {
  281. return self::$envConfig[$path];
  282. }
  283. if (!is_dir($path)) {
  284. self::$envConfig[$path] = [];
  285. return self::$envConfig[$path];
  286. }
  287. $envFiles = Finder::create()
  288. ->files()
  289. ->name('*.yml')
  290. ->in($path)
  291. ->depth('< 2');
  292. $envConfig = [];
  293. /** @var SplFileInfo $envFile */
  294. foreach ($envFiles as $envFile) {
  295. $env = str_replace(['.dist.yml', '.yml'], '', $envFile->getFilename());
  296. $envConfig[$env] = [];
  297. $envPath = $path;
  298. if ($envFile->getRelativePath()) {
  299. $envPath .= DIRECTORY_SEPARATOR . $envFile->getRelativePath();
  300. }
  301. foreach (['.dist.yml', '.yml'] as $suffix) {
  302. $envConf = self::getConfFromFile($envPath . DIRECTORY_SEPARATOR . $env . $suffix, null);
  303. if ($envConf === null) {
  304. continue;
  305. }
  306. $envConfig[$env] = self::mergeConfigs($envConfig[$env], $envConf);
  307. }
  308. }
  309. self::$envConfig[$path] = ['env' => $envConfig];
  310. return self::$envConfig[$path];
  311. }
  312. /**
  313. * Loads configuration from Yaml data
  314. *
  315. * @param string $contents Yaml config file contents
  316. * @param string $filename which is supposed to be loaded
  317. * @return array
  318. * @throws ConfigurationException
  319. */
  320. protected static function getConfFromContents($contents, $filename = '(.yml)')
  321. {
  322. if (self::$params) {
  323. $template = new Template($contents, '%', '%');
  324. $template->setVars(self::$params);
  325. $contents = $template->produce();
  326. }
  327. try {
  328. return Yaml::parse($contents);
  329. } catch (ParseException $exception) {
  330. throw new ConfigurationException(
  331. sprintf(
  332. "Error loading Yaml config from `%s`\n \n%s\nRead more about Yaml format https://goo.gl/9UPuEC",
  333. $filename,
  334. $exception->getMessage()
  335. )
  336. );
  337. }
  338. }
  339. /**
  340. * Loads configuration from Yaml file or returns given value if the file doesn't exist
  341. *
  342. * @param string $filename filename
  343. * @param mixed $nonExistentValue value used if filename is not found
  344. * @return array
  345. */
  346. protected static function getConfFromFile($filename, $nonExistentValue = [])
  347. {
  348. if (file_exists($filename)) {
  349. $yaml = file_get_contents($filename);
  350. return self::getConfFromContents($yaml, $filename);
  351. }
  352. return $nonExistentValue;
  353. }
  354. /**
  355. * Returns all possible suite configurations according environment rules.
  356. * Suite configurations will contain `current_environment` key which specifies what environment used.
  357. *
  358. * @param $suite
  359. * @return array
  360. */
  361. public static function suiteEnvironments($suite)
  362. {
  363. $settings = self::suiteSettings($suite, self::config());
  364. if (!isset($settings['env']) || !is_array($settings['env'])) {
  365. return [];
  366. }
  367. $environments = [];
  368. foreach ($settings['env'] as $env => $envConfig) {
  369. $environments[$env] = $envConfig ? self::mergeConfigs($settings, $envConfig) : $settings;
  370. $environments[$env]['current_environment'] = $env;
  371. }
  372. return $environments;
  373. }
  374. public static function suites()
  375. {
  376. return self::$suites;
  377. }
  378. /**
  379. * Return list of enabled modules according suite config.
  380. *
  381. * @param array $settings suite settings
  382. * @return array
  383. */
  384. public static function modules($settings)
  385. {
  386. return array_filter(
  387. array_map(
  388. function ($m) {
  389. return is_array($m) ? key($m) : $m;
  390. },
  391. $settings['modules']['enabled'],
  392. array_keys($settings['modules']['enabled'])
  393. ),
  394. function ($m) use ($settings) {
  395. if (!isset($settings['modules']['disabled'])) {
  396. return true;
  397. }
  398. return !in_array($m, $settings['modules']['disabled']);
  399. }
  400. );
  401. }
  402. public static function isExtensionEnabled($extensionName)
  403. {
  404. return isset(self::$config['extensions'])
  405. && isset(self::$config['extensions']['enabled'])
  406. && in_array($extensionName, self::$config['extensions']['enabled']);
  407. }
  408. /**
  409. * Returns current path to `_data` dir.
  410. * Use it to store database fixtures, sql dumps, or other files required by your tests.
  411. *
  412. * @return string
  413. */
  414. public static function dataDir()
  415. {
  416. return self::$dir . DIRECTORY_SEPARATOR . self::$dataDir . DIRECTORY_SEPARATOR;
  417. }
  418. /**
  419. * Return current path to `_helpers` dir.
  420. * Helpers are custom modules.
  421. *
  422. * @return string
  423. */
  424. public static function supportDir()
  425. {
  426. return self::$dir . DIRECTORY_SEPARATOR . self::$supportDir . DIRECTORY_SEPARATOR;
  427. }
  428. /**
  429. * Returns actual path to current `_output` dir.
  430. * Use it in Helpers or Groups to save result or temporary files.
  431. *
  432. * @return string
  433. * @throws Exception\ConfigurationException
  434. */
  435. public static function outputDir()
  436. {
  437. if (!self::$outputDir) {
  438. throw new ConfigurationException("Path for output not specified. Please, set output path in global config");
  439. }
  440. $dir = self::$outputDir . DIRECTORY_SEPARATOR;
  441. if (strcmp(self::$outputDir[0], "/") !== 0) {
  442. $dir = self::$dir . DIRECTORY_SEPARATOR . $dir;
  443. }
  444. if (!file_exists($dir)) {
  445. @mkdir($dir, 0777, true);
  446. }
  447. if (!is_writable($dir)) {
  448. @chmod($dir, 0777);
  449. }
  450. if (!is_writable($dir)) {
  451. throw new ConfigurationException(
  452. "Path for output is not writable. Please, set appropriate access mode for output path."
  453. );
  454. }
  455. return $dir;
  456. }
  457. /**
  458. * Compatibility alias to `Configuration::logDir()`
  459. * @return string
  460. */
  461. public static function logDir()
  462. {
  463. return self::outputDir();
  464. }
  465. /**
  466. * Returns path to the root of your project.
  467. * Basically returns path to current `codeception.yml` loaded.
  468. * Use this method instead of `__DIR__`, `getcwd()` or anything else.
  469. * @return string
  470. */
  471. public static function projectDir()
  472. {
  473. return self::$dir . DIRECTORY_SEPARATOR;
  474. }
  475. /**
  476. * Returns path to tests directory
  477. *
  478. * @return string
  479. */
  480. public static function testsDir()
  481. {
  482. return self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR;
  483. }
  484. /**
  485. * Return current path to `_envs` dir.
  486. * Use it to store environment specific configuration.
  487. *
  488. * @return string
  489. */
  490. public static function envsDir()
  491. {
  492. if (!self::$envsDir) {
  493. return null;
  494. }
  495. return self::$dir . DIRECTORY_SEPARATOR . self::$envsDir . DIRECTORY_SEPARATOR;
  496. }
  497. /**
  498. * Is this a meta-configuration file that just points to other `codeception.yml`?
  499. * If so, it may have no tests by itself.
  500. *
  501. * @return bool
  502. */
  503. public static function isEmpty()
  504. {
  505. return !(bool)self::$testsDir;
  506. }
  507. /**
  508. * Adds parameters to config
  509. *
  510. * @param array $config
  511. * @return array
  512. */
  513. public static function append(array $config = [])
  514. {
  515. self::$config = self::mergeConfigs(self::$config, $config);
  516. if (isset(self::$config['paths']['output'])) {
  517. self::$outputDir = self::$config['paths']['output'];
  518. }
  519. if (isset(self::$config['paths']['data'])) {
  520. self::$dataDir = self::$config['paths']['data'];
  521. }
  522. if (isset(self::$config['paths']['support'])) {
  523. self::$supportDir = self::$config['paths']['support'];
  524. }
  525. if (isset(self::$config['paths']['tests'])) {
  526. self::$testsDir = self::$config['paths']['tests'];
  527. }
  528. return self::$config;
  529. }
  530. public static function mergeConfigs($a1, $a2)
  531. {
  532. if (!is_array($a1)) {
  533. return $a2;
  534. }
  535. if (!is_array($a2)) {
  536. return $a1;
  537. }
  538. $res = [];
  539. // for sequential arrays
  540. if (isset($a1[0]) && isset($a2[0])) {
  541. return array_merge_recursive($a2, $a1);
  542. }
  543. // for associative arrays
  544. foreach ($a2 as $k2 => $v2) {
  545. if (!isset($a1[$k2])) { // if no such key
  546. $res[$k2] = $v2;
  547. unset($a1[$k2]);
  548. continue;
  549. }
  550. $res[$k2] = self::mergeConfigs($a1[$k2], $v2);
  551. unset($a1[$k2]);
  552. }
  553. foreach ($a1 as $k1 => $v1) { // only single elements here left
  554. $res[$k1] = $v1;
  555. }
  556. return $res;
  557. }
  558. /**
  559. * Loads config from *.dist.suite.yml and *.suite.yml
  560. *
  561. * @param $suite
  562. * @param $path
  563. * @param $settings
  564. * @return array
  565. */
  566. protected static function loadSuiteConfig($suite, $path, $settings)
  567. {
  568. if (isset(self::$config['suites'][$suite])) {
  569. // bundled config
  570. return self::mergeConfigs($settings, self::$config['suites'][$suite]);
  571. }
  572. $suiteDistConf = self::getConfFromFile(
  573. self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.dist.yml"
  574. );
  575. $suiteConf = self::getConfFromFile(
  576. self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.yml"
  577. );
  578. $settings = self::mergeConfigs($settings, $suiteDistConf);
  579. $settings = self::mergeConfigs($settings, $suiteConf);
  580. return $settings;
  581. }
  582. /**
  583. * Replaces wildcarded items in include array with real paths.
  584. *
  585. * @param $includes
  586. * @return array
  587. */
  588. protected static function expandWildcardedIncludes(array $includes)
  589. {
  590. if (empty($includes)) {
  591. return $includes;
  592. }
  593. $expandedIncludes = [];
  594. foreach ($includes as $include) {
  595. $expandedIncludes = array_merge($expandedIncludes, self::expandWildcardsFor($include));
  596. }
  597. return $expandedIncludes;
  598. }
  599. /**
  600. * Finds config files in given wildcarded include path.
  601. * Returns the expanded paths or the original if not a wildcard.
  602. *
  603. * @param $include
  604. * @return array
  605. * @throws ConfigurationException
  606. */
  607. protected static function expandWildcardsFor($include)
  608. {
  609. if (1 !== preg_match('/[\?\.\*]/', $include)) {
  610. return [$include,];
  611. }
  612. try {
  613. $configFiles = Finder::create()->files()
  614. ->name('/codeception(\.dist\.yml|\.yml)/')
  615. ->in(self::$dir . DIRECTORY_SEPARATOR . $include);
  616. } catch (\InvalidArgumentException $e) {
  617. throw new ConfigurationException(
  618. "Configuration file(s) could not be found in \"$include\"."
  619. );
  620. }
  621. $paths = [];
  622. foreach ($configFiles as $file) {
  623. $paths[] = codecept_relative_path($file->getPath());
  624. }
  625. return $paths;
  626. }
  627. private static function prepareParams($settings)
  628. {
  629. self::$params = [];
  630. $paramsLoader = new ParamsLoader();
  631. foreach ($settings['params'] as $paramStorage) {
  632. static::$params = array_merge(self::$params, $paramsLoader->load($paramStorage));
  633. }
  634. }
  635. }