PageRenderTime 48ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Codeception/Configuration.php

https://github.com/pmcjury/Codeception
PHP | 491 lines | 363 code | 57 blank | 71 comment | 26 complexity | ab853d08fc4bf81f2a37a3f7ed126d4b MD5 | raw file
  1. <?php
  2. namespace Codeception;
  3. use Codeception\Exception\Configuration as ConfigurationException;
  4. use Codeception\Util\Autoload;
  5. use Symfony\Component\Yaml\Yaml;
  6. use Symfony\Component\Finder\Finder;
  7. class Configuration
  8. {
  9. protected static $suites = array();
  10. /**
  11. * @var array Current configuration
  12. */
  13. protected static $config = null;
  14. /**
  15. * @var string Directory containing main configuration file.
  16. * @see self::projectDir()
  17. */
  18. protected static $dir = null;
  19. /**
  20. * @var string Current project logs directory.
  21. */
  22. protected static $logDir = null;
  23. /**
  24. * @var string Current project data directory. This directory is used to hold
  25. * sql dumps and other things needed for current project tests.
  26. */
  27. protected static $dataDir = null;
  28. /**
  29. * @var string Directory containing helpers. Helpers will be autoloaded if they have suffix "Helper".
  30. */
  31. protected static $helpersDir = null;
  32. /**
  33. * @var string Directory containing tests and suites of the current project.
  34. */
  35. protected static $testsDir = null;
  36. public static $lock = false;
  37. /**
  38. * @var array Default config
  39. */
  40. public static $defaultConfig = array(
  41. 'actor' => 'Guy',
  42. 'namespace' => '',
  43. 'include' => array(),
  44. 'paths' => array(),
  45. 'modules' => array(),
  46. 'extensions' => array(
  47. 'enabled' => array(),
  48. 'config' => array(),
  49. ),
  50. 'groups' => [],
  51. 'settings' => array(
  52. 'colors' => false,
  53. 'log' => false,
  54. 'bootstrap' => '_bootstrap.php',
  55. ),
  56. 'coverage' => []
  57. );
  58. public static $defaultSuiteSettings = array(
  59. 'class_name' => 'NoGuy',
  60. 'modules' => array(
  61. 'enabled' => array(),
  62. 'config' => array(),
  63. ),
  64. 'namespace' => null,
  65. 'path' => '',
  66. 'groups' => [],
  67. 'suite_class' => '\PHPUnit_Framework_TestSuite',
  68. 'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED',
  69. );
  70. /**
  71. * Loads global config file which is `codeception.yml` by default.
  72. * When config is already loaded - returns it.
  73. *
  74. * @param null $configFile
  75. * @return array
  76. * @throws Exception\Configuration
  77. */
  78. public static function config($configFile = null)
  79. {
  80. if (!$configFile && self::$config) {
  81. return self::$config;
  82. }
  83. if (self::$config && self::$lock) {
  84. return self::$config;
  85. }
  86. if ($configFile === null) {
  87. $configFile = getcwd() . DIRECTORY_SEPARATOR . 'codeception.yml';
  88. }
  89. if (is_dir($configFile)) {
  90. $configFile = $configFile . DIRECTORY_SEPARATOR . 'codeception.yml';
  91. }
  92. $dir = realpath(dirname($configFile));
  93. $configDistFile = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml';
  94. if (! (file_exists($configDistFile) || file_exists($configFile))) {
  95. throw new ConfigurationException("Configuration file could not be found");
  96. }
  97. $config = self::loadConfigFile($configDistFile, self::$defaultConfig);
  98. $config = self::loadConfigFile($configFile, $config);
  99. if ($config == self::$defaultConfig) {
  100. throw new ConfigurationException("Configuration file is invalid");
  101. }
  102. self::$dir = $dir;
  103. self::$config = $config;
  104. if (!isset($config['paths']['log'])) {
  105. throw new ConfigurationException('Log path is not defined by key "paths: log"');
  106. }
  107. self::$logDir = $config['paths']['log'];
  108. // config without tests, for inclusion of other configs
  109. if (count($config['include']) and !isset($config['paths']['tests'])) {
  110. return $config;
  111. }
  112. if (!isset($config['paths']['tests'])) {
  113. throw new ConfigurationException('Tests directory is not defined in Codeception config by key "paths: tests:"');
  114. }
  115. if (!isset($config['paths']['data'])) {
  116. throw new ConfigurationException('Data path is not defined Codeception config by key "paths: data"');
  117. }
  118. if (!isset($config['paths']['helpers'])) {
  119. throw new ConfigurationException('Helpers path is not defined by key "paths: helpers"');
  120. }
  121. self::$dataDir = $config['paths']['data'];
  122. self::$helpersDir = $config['paths']['helpers'];
  123. self::$testsDir = $config['paths']['tests'];
  124. self::loadBootstrap($config['settings']['bootstrap']);
  125. self::autoloadHelpers();
  126. self::loadSuites();
  127. return $config;
  128. }
  129. protected static function loadBootstrap($bootstrap)
  130. {
  131. if (!$bootstrap) {
  132. return;
  133. }
  134. $bootstrap = self::$dir . DIRECTORY_SEPARATOR . self::$testsDir.DIRECTORY_SEPARATOR.$bootstrap;
  135. if (file_exists($bootstrap)) {
  136. include_once $bootstrap;
  137. }
  138. }
  139. protected static function loadConfigFile($file, $parentConfig)
  140. {
  141. $config = file_exists($file) ? Yaml::parse($file) : array();
  142. return self::mergeConfigs($parentConfig, $config);
  143. }
  144. protected static function autoloadHelpers()
  145. {
  146. Autoload::registerSuffix('Helper', self::helpersDir());
  147. }
  148. protected static function loadSuites()
  149. {
  150. $suites = Finder::create()->files()->name('*.{suite,suite.dist}.yml')->in(self::$dir.DIRECTORY_SEPARATOR.self::$testsDir)->depth('< 1');
  151. self::$suites = array();
  152. foreach ($suites as $suite) {
  153. preg_match('~(.*?)(\.suite|\.suite\.dist)\.yml~', $suite->getFilename(), $matches);
  154. self::$suites[$matches[1]] = $matches[1];
  155. }
  156. }
  157. /**
  158. * Returns suite configuration. Requires suite name and global config used (Configuration::config)
  159. *
  160. * @param $suite
  161. * @param $config
  162. * @return array
  163. * @throws \Exception
  164. */
  165. public static function suiteSettings($suite, $config)
  166. {
  167. // cut namespace name from suite name
  168. if ($suite != $config['namespace'] && substr($suite, 0, strlen($config['namespace'])) == $config['namespace']) {
  169. $suite = substr($suite, strlen($config['namespace']));
  170. }
  171. if (!in_array($suite, self::$suites)) {
  172. throw new \Exception("Suite $suite was not loaded");
  173. }
  174. $globalConf = $config['settings'];
  175. foreach (array('modules','coverage', 'namespace', 'groups', 'env') as $key) {
  176. if (isset($config[$key])) {
  177. $globalConf[$key] = $config[$key];
  178. }
  179. }
  180. $path = $config['paths']['tests'];
  181. $suiteConf = file_exists(self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.yml") ? Yaml::parse(self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.yml") : array();
  182. $suiteDistconf = file_exists(self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.dist.yml") ? Yaml::parse(self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . "$suite.suite.dist.yml") : array();
  183. $settings = self::mergeConfigs(self::$defaultSuiteSettings, $globalConf);
  184. $settings = self::mergeConfigs($settings, $suiteDistconf);
  185. $settings = self::mergeConfigs($settings, $suiteConf);
  186. $settings['path'] = self::$dir . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . $suite . DIRECTORY_SEPARATOR;
  187. return $settings;
  188. }
  189. /**
  190. * Returns all possible suite configurations according environment rules.
  191. * Suite configurations will contain `current_environment` key which specifies what environment used.
  192. *
  193. * @param $suite
  194. * @return array
  195. */
  196. public static function suiteEnvironments($suite)
  197. {
  198. $settings = self::suiteSettings($suite, self::config());
  199. if (!isset($settings['env']) || !is_array($settings['env'])) {
  200. return array();
  201. }
  202. $environments = array();
  203. foreach ($settings['env'] as $env => $envConfig) {
  204. $environments[$env] = $envConfig ? self::mergeConfigs($settings, $envConfig) : $settings;
  205. $environments[$env]['current_environment'] = $env;
  206. }
  207. return $environments;
  208. }
  209. public static function suites()
  210. {
  211. return self::$suites;
  212. }
  213. /**
  214. * Return instances of enabled modules according suite config.
  215. * Requires Guy class if it exists.
  216. *
  217. * @param array $settings suite settings
  218. * @return array|\Codeception\Module[]
  219. */
  220. public static function modules($settings)
  221. {
  222. $modules = array();
  223. $namespace = isset($settings['namespace']) ? $settings['namespace'] : '';
  224. $moduleNames = $settings['modules']['enabled'];
  225. foreach ($moduleNames as $moduleName) {
  226. $moduleConfig = (isset($settings['modules']['config'][$moduleName])) ? $settings['modules']['config'][$moduleName] : array();
  227. $modules[$moduleName] = static::createModule($moduleName, $moduleConfig, $namespace);
  228. }
  229. return $modules;
  230. }
  231. /**
  232. * Creates new module and configures it.
  233. * Module class is searched and resolves according following rules:
  234. *
  235. * 1. if "class" element is fully qualified class name, it will be taken to create module;
  236. * 2. module class will be searched under default namespace, according $namespace parameter:
  237. * $namespace.'\Codeception\Module\' . $class;
  238. * 3. module class will be searched under Codeception module namespace, that is "\Codeception\Module".
  239. *
  240. * @param $class
  241. * @param array $config module configuration
  242. * @param string $namespace default namespace for module.
  243. * @throws Exception\Configuration
  244. * @return \Codeception\Module
  245. */
  246. public static function createModule($class, $config, $namespace = '')
  247. {
  248. $hasNamespace = (mb_strpos($class, '\\') !== false);
  249. if ($hasNamespace) {
  250. return new $class($config);
  251. }
  252. // try find module under users suite namespace setting
  253. $className = $namespace.'\\Codeception\\Module\\' . $class;
  254. if (!@class_exists($className)) {
  255. // fallback to default namespace
  256. $className = '\\Codeception\\Module\\' . $class;
  257. if (!@class_exists($className)) {
  258. throw new ConfigurationException($class.' could not be found and loaded');
  259. }
  260. }
  261. return new $className($config);
  262. }
  263. public static function isExtensionEnabled($extensionName)
  264. {
  265. return isset(self::$config['extensions'])
  266. && isset(self::$config['extensions']['enabled'])
  267. && in_array($extensionName, self::$config['extensions']['enabled']);
  268. }
  269. public static function actions($modules)
  270. {
  271. $actions = array();
  272. foreach ($modules as $moduleName => $module) {
  273. $class = new \ReflectionClass($module);
  274. $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
  275. foreach ($methods as $method) {
  276. $inherit = $class->getStaticPropertyValue('includeInheritedActions');
  277. $only = $class->getStaticPropertyValue('onlyActions');
  278. $exclude = $class->getStaticPropertyValue('excludeActions');
  279. // exclude methods when they are listed as excluded
  280. if (in_array($method->name, $exclude)) continue;
  281. if (!empty($only)) {
  282. // skip if method is not listed
  283. if (!in_array($method->name, $only)) continue;
  284. } else {
  285. // skip if method is inherited and inheritActions == false
  286. if (!$inherit and $method->getDeclaringClass() != $class) continue;
  287. }
  288. // those with underscore at the beginning are considered as hidden
  289. if (strpos($method->name, '_') === 0) continue;
  290. $actions[$method->name] = $moduleName;
  291. }
  292. }
  293. return $actions;
  294. }
  295. /**
  296. * Returns current path to `_data` dir.
  297. * Use it to store database fixtures, sql dumps, or other files required by your tests.
  298. *
  299. * @return string
  300. */
  301. public static function dataDir()
  302. {
  303. return self::$dir . DIRECTORY_SEPARATOR . self::$dataDir . DIRECTORY_SEPARATOR;
  304. }
  305. /**
  306. * Return current path to `_helpers` dir.
  307. * Helpers are custom modules.
  308. *
  309. * @return string
  310. */
  311. public static function helpersDir()
  312. {
  313. return self::$dir . DIRECTORY_SEPARATOR . self::$helpersDir . DIRECTORY_SEPARATOR;
  314. }
  315. /**
  316. * Returns actual path to current `_output` dir.
  317. * Use it in Helpers or Groups to save result or temporary files.
  318. *
  319. * @return string
  320. * @throws Exception\Configuration
  321. */
  322. public static function outputDir()
  323. {
  324. if (!self::$logDir) {
  325. throw new ConfigurationException("Path for logs not specified. Please, set log path in global config");
  326. }
  327. $dir = self::$dir . DIRECTORY_SEPARATOR . self::$logDir . DIRECTORY_SEPARATOR;
  328. if (!is_writable($dir)) {
  329. @mkdir($dir);
  330. @chmod($dir, 0777);
  331. }
  332. if (!is_writable($dir)) {
  333. throw new ConfigurationException("Path for logs is not writable. Please, set appropriate access mode for log path.");
  334. }
  335. return $dir;
  336. }
  337. /**
  338. * Compatibility alias to `Configuration::logDir()`
  339. * @return string
  340. */
  341. public static function logDir()
  342. {
  343. return self::outputDir();
  344. }
  345. /**
  346. * Returns path to the root of your project.
  347. * Basically returns path to current `codeception.yml` loaded.
  348. * Use this method instead of `__DIR__`, `getcwd()` or anything else.
  349. * @return string
  350. */
  351. public static function projectDir()
  352. {
  353. return self::$dir . DIRECTORY_SEPARATOR;
  354. }
  355. /**
  356. * Returns path to tests directory
  357. *
  358. * @return string
  359. */
  360. public static function testsDir()
  361. {
  362. return self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR;
  363. }
  364. /**
  365. * Is this a meta-configuration file that just points to other `codeception.yml`?
  366. * If so, it may have no tests by itself.
  367. *
  368. * @return bool
  369. */
  370. public static function isEmpty()
  371. {
  372. return !(bool)self::$testsDir;
  373. }
  374. /**
  375. * Adds parameters to config
  376. *
  377. * @param array $config
  378. * @return array
  379. */
  380. public static function append(array $config = array())
  381. {
  382. return self::$config = self::mergeConfigs(self::$config, $config);
  383. }
  384. public static function mergeConfigs($a1, $a2)
  385. {
  386. if (!is_array($a1) || !is_array($a2)) {
  387. return $a2;
  388. }
  389. $res = array();
  390. foreach ($a2 as $k2 => $v2) {
  391. if (!isset($a1[$k2])) { // if no such key
  392. $res[$k2] = $v2;
  393. unset($a1[$k2]);
  394. continue;
  395. }
  396. $res[$k2] = self::mergeConfigs($a1[$k2], $v2);
  397. unset($a1[$k2]);
  398. }
  399. foreach ($a1 as $k1 => $v1) { // only single elements here left
  400. $res[$k1] = $v1;
  401. }
  402. return $res;
  403. }
  404. }