TestTask.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. /**
  3. * The TestTask handles creating and updating test files.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * For full copyright and license information, please see the LICENSE.txt
  12. * Redistributions of files must retain the above copyright notice.
  13. *
  14. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  15. * @link http://cakephp.org CakePHP(tm) Project
  16. * @since CakePHP(tm) v 1.3
  17. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  18. */
  19. App::uses('AppShell', 'Console/Command');
  20. App::uses('BakeTask', 'Console/Command/Task');
  21. App::uses('ClassRegistry', 'Utility');
  22. /**
  23. * Task class for creating and updating test files.
  24. *
  25. * @package Cake.Console.Command.Task
  26. */
  27. class TestTask extends BakeTask {
  28. /**
  29. * path to TESTS directory
  30. *
  31. * @var string
  32. */
  33. public $path = TESTS;
  34. /**
  35. * Tasks used.
  36. *
  37. * @var array
  38. */
  39. public $tasks = array('Template');
  40. /**
  41. * class types that methods can be generated for
  42. *
  43. * @var array
  44. */
  45. public $classTypes = array(
  46. 'Model' => 'Model',
  47. 'Controller' => 'Controller',
  48. 'Component' => 'Controller/Component',
  49. 'Behavior' => 'Model/Behavior',
  50. 'Helper' => 'View/Helper'
  51. );
  52. /**
  53. * Mapping between packages, and their baseclass + package.
  54. * This is used to generate App::uses() call to autoload base
  55. * classes if a developer has forgotten to do so.
  56. *
  57. * @var array
  58. */
  59. public $baseTypes = array(
  60. 'Model' => array('Model', 'Model'),
  61. 'Behavior' => array('ModelBehavior', 'Model'),
  62. 'Controller' => array('Controller', 'Controller'),
  63. 'Component' => array('Component', 'Controller'),
  64. 'Helper' => array('Helper', 'View')
  65. );
  66. /**
  67. * Internal list of fixtures that have been added so far.
  68. *
  69. * @var array
  70. */
  71. protected $_fixtures = array();
  72. /**
  73. * Execution method always used for tasks
  74. *
  75. * @return void
  76. */
  77. public function execute() {
  78. parent::execute();
  79. $count = count($this->args);
  80. if (!$count) {
  81. $this->_interactive();
  82. }
  83. if ($count === 1) {
  84. $this->_interactive($this->args[0]);
  85. }
  86. if ($count > 1) {
  87. $type = Inflector::classify($this->args[0]);
  88. if ($this->bake($type, $this->args[1])) {
  89. $this->out('<success>Done</success>');
  90. }
  91. }
  92. }
  93. /**
  94. * Handles interactive baking
  95. *
  96. * @param string $type
  97. * @return string|boolean
  98. */
  99. protected function _interactive($type = null) {
  100. $this->interactive = true;
  101. $this->hr();
  102. $this->out(__d('cake_console', 'Bake Tests'));
  103. $this->out(__d('cake_console', 'Path: %s', $this->getPath()));
  104. $this->hr();
  105. if ($type) {
  106. $type = Inflector::camelize($type);
  107. if (!isset($this->classTypes[$type])) {
  108. $this->error(__d('cake_console', 'Incorrect type provided. Please choose one of %s', implode(', ', array_keys($this->classTypes))));
  109. }
  110. } else {
  111. $type = $this->getObjectType();
  112. }
  113. $className = $this->getClassName($type);
  114. return $this->bake($type, $className);
  115. }
  116. /**
  117. * Completes final steps for generating data to create test case.
  118. *
  119. * @param string $type Type of object to bake test case for ie. Model, Controller
  120. * @param string $className the 'cake name' for the class ie. Posts for the PostsController
  121. * @return string|boolean
  122. */
  123. public function bake($type, $className) {
  124. $plugin = null;
  125. if ($this->plugin) {
  126. $plugin = $this->plugin . '.';
  127. }
  128. $realType = $this->mapType($type, $plugin);
  129. $fullClassName = $this->getRealClassName($type, $className);
  130. if ($this->typeCanDetectFixtures($type) && $this->isLoadableClass($realType, $fullClassName)) {
  131. $this->out(__d('cake_console', 'Bake is detecting possible fixtures...'));
  132. $testSubject = $this->buildTestSubject($type, $className);
  133. $this->generateFixtureList($testSubject);
  134. } elseif ($this->interactive) {
  135. $this->getUserFixtures();
  136. }
  137. list($baseClass, $baseType) = $this->getBaseType($type);
  138. App::uses($baseClass, $baseType);
  139. App::uses($fullClassName, $realType);
  140. $methods = array();
  141. if (class_exists($fullClassName)) {
  142. $methods = $this->getTestableMethods($fullClassName);
  143. }
  144. $mock = $this->hasMockClass($type, $fullClassName);
  145. list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName, $plugin);
  146. $uses = $this->generateUses($type, $realType, $fullClassName);
  147. $this->out("\n" . __d('cake_console', 'Baking test case for %s %s ...', $className, $type), 1, Shell::QUIET);
  148. $this->Template->set('fixtures', $this->_fixtures);
  149. $this->Template->set('plugin', $plugin);
  150. $this->Template->set(compact(
  151. 'className', 'methods', 'type', 'fullClassName', 'mock',
  152. 'realType', 'preConstruct', 'postConstruct', 'construction',
  153. 'uses'
  154. ));
  155. $out = $this->Template->generate('classes', 'test');
  156. $filename = $this->testCaseFileName($type, $className);
  157. $made = $this->createFile($filename, $out);
  158. if ($made) {
  159. return $out;
  160. }
  161. return false;
  162. }
  163. /**
  164. * Interact with the user and get their chosen type. Can exit the script.
  165. *
  166. * @return string Users chosen type.
  167. */
  168. public function getObjectType() {
  169. $this->hr();
  170. $this->out(__d('cake_console', 'Select an object type:'));
  171. $this->hr();
  172. $keys = array();
  173. $i = 0;
  174. foreach ($this->classTypes as $option => $package) {
  175. $this->out(++$i . '. ' . $option);
  176. $keys[] = $i;
  177. }
  178. $keys[] = 'q';
  179. $selection = $this->in(__d('cake_console', 'Enter the type of object to bake a test for or (q)uit'), $keys, 'q');
  180. if ($selection === 'q') {
  181. return $this->_stop();
  182. }
  183. $types = array_keys($this->classTypes);
  184. return $types[$selection - 1];
  185. }
  186. /**
  187. * Get the user chosen Class name for the chosen type
  188. *
  189. * @param string $objectType Type of object to list classes for i.e. Model, Controller.
  190. * @return string Class name the user chose.
  191. */
  192. public function getClassName($objectType) {
  193. $type = ucfirst(strtolower($objectType));
  194. $typeLength = strlen($type);
  195. $type = $this->classTypes[$type];
  196. if ($this->plugin) {
  197. $plugin = $this->plugin . '.';
  198. $options = App::objects($plugin . $type);
  199. } else {
  200. $options = App::objects($type);
  201. }
  202. $this->out(__d('cake_console', 'Choose a %s class', $objectType));
  203. $keys = array();
  204. foreach ($options as $key => $option) {
  205. $this->out(++$key . '. ' . $option);
  206. $keys[] = $key;
  207. }
  208. while (empty($selection)) {
  209. $selection = $this->in(__d('cake_console', 'Choose an existing class, or enter the name of a class that does not exist'));
  210. if (is_numeric($selection) && isset($options[$selection - 1])) {
  211. $selection = $options[$selection - 1];
  212. }
  213. if ($type !== 'Model') {
  214. $selection = substr($selection, 0, $typeLength * - 1);
  215. }
  216. }
  217. return $selection;
  218. }
  219. /**
  220. * Checks whether the chosen type can find its own fixtures.
  221. * Currently only model, and controller are supported
  222. *
  223. * @param string $type The Type of object you are generating tests for eg. controller
  224. * @return boolean
  225. */
  226. public function typeCanDetectFixtures($type) {
  227. $type = strtolower($type);
  228. return in_array($type, array('controller', 'model'));
  229. }
  230. /**
  231. * Check if a class with the given package is loaded or can be loaded.
  232. *
  233. * @param string $package The package of object you are generating tests for eg. controller
  234. * @param string $class the Classname of the class the test is being generated for.
  235. * @return boolean
  236. */
  237. public function isLoadableClass($package, $class) {
  238. App::uses($class, $package);
  239. list($plugin, $ns) = pluginSplit($package);
  240. if ($plugin) {
  241. App::uses("{$plugin}AppController", $package);
  242. App::uses("{$plugin}AppModel", $package);
  243. App::uses("{$plugin}AppHelper", $package);
  244. }
  245. return class_exists($class);
  246. }
  247. /**
  248. * Construct an instance of the class to be tested.
  249. * So that fixtures can be detected
  250. *
  251. * @param string $type The Type of object you are generating tests for eg. controller
  252. * @param string $class the Classname of the class the test is being generated for.
  253. * @return object And instance of the class that is going to be tested.
  254. */
  255. public function buildTestSubject($type, $class) {
  256. ClassRegistry::flush();
  257. App::uses($class, $type);
  258. $class = $this->getRealClassName($type, $class);
  259. if (strtolower($type) === 'model') {
  260. $instance = ClassRegistry::init($class);
  261. } else {
  262. $instance = new $class();
  263. }
  264. return $instance;
  265. }
  266. /**
  267. * Gets the real class name from the cake short form. If the class name is already
  268. * suffixed with the type, the type will not be duplicated.
  269. *
  270. * @param string $type The Type of object you are generating tests for eg. controller
  271. * @param string $class the Classname of the class the test is being generated for.
  272. * @return string Real classname
  273. */
  274. public function getRealClassName($type, $class) {
  275. if (strtolower($type) === 'model' || empty($this->classTypes[$type])) {
  276. return $class;
  277. }
  278. $position = strpos($class, $type);
  279. if ($position !== false && strlen($class) - $position == strlen($type)) {
  280. return $class;
  281. }
  282. return $class . $type;
  283. }
  284. /**
  285. * Map the types that TestTask uses to concrete types that App::uses can use.
  286. *
  287. * @param string $type The type of thing having a test generated.
  288. * @param string $plugin The plugin name.
  289. * @return string
  290. * @throws CakeException When invalid object types are requested.
  291. */
  292. public function mapType($type, $plugin) {
  293. $type = ucfirst($type);
  294. if (empty($this->classTypes[$type])) {
  295. throw new CakeException(__d('cake_dev', 'Invalid object type.'));
  296. }
  297. $real = $this->classTypes[$type];
  298. if ($plugin) {
  299. $real = trim($plugin, '.') . '.' . $real;
  300. }
  301. return $real;
  302. }
  303. /**
  304. * Get the base class and package name for a given type.
  305. *
  306. * @param string $type The type the class having a test
  307. * generated for is in.
  308. * @return array Array of (class, type)
  309. * @throws CakeException on invalid types.
  310. */
  311. public function getBaseType($type) {
  312. if (empty($this->baseTypes[$type])) {
  313. throw new CakeException(__d('cake_dev', 'Invalid type name'));
  314. }
  315. return $this->baseTypes[$type];
  316. }
  317. /**
  318. * Get methods declared in the class given.
  319. * No parent methods will be returned
  320. *
  321. * @param string $className Name of class to look at.
  322. * @return array Array of method names.
  323. */
  324. public function getTestableMethods($className) {
  325. $classMethods = get_class_methods($className);
  326. $parentMethods = get_class_methods(get_parent_class($className));
  327. $thisMethods = array_diff($classMethods, $parentMethods);
  328. $out = array();
  329. foreach ($thisMethods as $method) {
  330. if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) {
  331. $out[] = $method;
  332. }
  333. }
  334. return $out;
  335. }
  336. /**
  337. * Generate the list of fixtures that will be required to run this test based on
  338. * loaded models.
  339. *
  340. * @param object $subject The object you want to generate fixtures for.
  341. * @return array Array of fixtures to be included in the test.
  342. */
  343. public function generateFixtureList($subject) {
  344. $this->_fixtures = array();
  345. if ($subject instanceof Model) {
  346. $this->_processModel($subject);
  347. } elseif ($subject instanceof Controller) {
  348. $this->_processController($subject);
  349. }
  350. return array_values($this->_fixtures);
  351. }
  352. /**
  353. * Process a model recursively and pull out all the
  354. * model names converting them to fixture names.
  355. *
  356. * @param Model $subject A Model class to scan for associations and pull fixtures off of.
  357. * @return void
  358. */
  359. protected function _processModel($subject) {
  360. $this->_addFixture($subject->name);
  361. $associated = $subject->getAssociated();
  362. foreach ($associated as $alias => $type) {
  363. $className = $subject->{$alias}->name;
  364. if (!isset($this->_fixtures[$className])) {
  365. $this->_processModel($subject->{$alias});
  366. }
  367. if ($type === 'hasAndBelongsToMany') {
  368. if (!empty($subject->hasAndBelongsToMany[$alias]['with'])) {
  369. list(, $joinModel) = pluginSplit($subject->hasAndBelongsToMany[$alias]['with']);
  370. } else {
  371. $joinModel = Inflector::classify($subject->hasAndBelongsToMany[$alias]['joinTable']);
  372. }
  373. if (!isset($this->_fixtures[$joinModel])) {
  374. $this->_processModel($subject->{$joinModel});
  375. }
  376. }
  377. }
  378. }
  379. /**
  380. * Process all the models attached to a controller
  381. * and generate a fixture list.
  382. *
  383. * @param Controller $subject A controller to pull model names off of.
  384. * @return void
  385. */
  386. protected function _processController($subject) {
  387. $subject->constructClasses();
  388. $models = array(Inflector::classify($subject->name));
  389. if (!empty($subject->uses)) {
  390. $models = $subject->uses;
  391. }
  392. foreach ($models as $model) {
  393. list(, $model) = pluginSplit($model);
  394. $this->_processModel($subject->{$model});
  395. }
  396. }
  397. /**
  398. * Add classname to the fixture list.
  399. * Sets the app. or plugin.plugin_name. prefix.
  400. *
  401. * @param string $name Name of the Model class that a fixture might be required for.
  402. * @return void
  403. */
  404. protected function _addFixture($name) {
  405. if ($this->plugin) {
  406. $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.';
  407. } else {
  408. $prefix = 'app.';
  409. }
  410. $fixture = $prefix . Inflector::underscore($name);
  411. $this->_fixtures[$name] = $fixture;
  412. }
  413. /**
  414. * Interact with the user to get additional fixtures they want to use.
  415. *
  416. * @return array Array of fixtures the user wants to add.
  417. */
  418. public function getUserFixtures() {
  419. $proceed = $this->in(__d('cake_console', 'Bake could not detect fixtures, would you like to add some?'), array('y', 'n'), 'n');
  420. $fixtures = array();
  421. if (strtolower($proceed) === 'y') {
  422. $fixtureList = $this->in(__d('cake_console', "Please provide a comma separated list of the fixtures names you'd like to use.\nExample: 'app.comment, app.post, plugin.forums.post'"));
  423. $fixtureListTrimmed = str_replace(' ', '', $fixtureList);
  424. $fixtures = explode(',', $fixtureListTrimmed);
  425. }
  426. $this->_fixtures = array_merge($this->_fixtures, $fixtures);
  427. return $fixtures;
  428. }
  429. /**
  430. * Is a mock class required for this type of test?
  431. * Controllers require a mock class.
  432. *
  433. * @param string $type The type of object tests are being generated for eg. controller.
  434. * @return boolean
  435. */
  436. public function hasMockClass($type) {
  437. $type = strtolower($type);
  438. return $type === 'controller';
  439. }
  440. /**
  441. * Generate a constructor code snippet for the type and classname
  442. *
  443. * @param string $type The Type of object you are generating tests for eg. controller
  444. * @param string $fullClassName The Classname of the class the test is being generated for.
  445. * @param string $plugin The plugin name.
  446. * @return array Constructor snippets for the thing you are building.
  447. */
  448. public function generateConstructor($type, $fullClassName, $plugin) {
  449. $type = strtolower($type);
  450. $pre = $construct = $post = '';
  451. if ($type === 'model') {
  452. $construct = "ClassRegistry::init('{$plugin}$fullClassName');\n";
  453. }
  454. if ($type === 'behavior') {
  455. $construct = "new $fullClassName();\n";
  456. }
  457. if ($type === 'helper') {
  458. $pre = "\$View = new View();\n";
  459. $construct = "new {$fullClassName}(\$View);\n";
  460. }
  461. if ($type === 'component') {
  462. $pre = "\$Collection = new ComponentCollection();\n";
  463. $construct = "new {$fullClassName}(\$Collection);\n";
  464. }
  465. return array($pre, $construct, $post);
  466. }
  467. /**
  468. * Generate the uses() calls for a type & classname
  469. *
  470. * @param string $type The Type of object you are generating tests for eg. controller
  471. * @param string $realType The package name for the class.
  472. * @param string $className The Classname of the class the test is being generated for.
  473. * @return array An array containing used classes
  474. */
  475. public function generateUses($type, $realType, $className) {
  476. $uses = array();
  477. $type = strtolower($type);
  478. if ($type === 'component') {
  479. $uses[] = array('ComponentCollection', 'Controller');
  480. $uses[] = array('Component', 'Controller');
  481. }
  482. if ($type === 'helper') {
  483. $uses[] = array('View', 'View');
  484. $uses[] = array('Helper', 'View');
  485. }
  486. $uses[] = array($className, $realType);
  487. return $uses;
  488. }
  489. /**
  490. * Make the filename for the test case. resolve the suffixes for controllers
  491. * and get the plugin path if needed.
  492. *
  493. * @param string $type The Type of object you are generating tests for eg. controller
  494. * @param string $className the Classname of the class the test is being generated for.
  495. * @return string filename the test should be created on.
  496. */
  497. public function testCaseFileName($type, $className) {
  498. $path = $this->getPath() . 'Case' . DS;
  499. $type = Inflector::camelize($type);
  500. if (isset($this->classTypes[$type])) {
  501. $path .= $this->classTypes[$type] . DS;
  502. }
  503. $className = $this->getRealClassName($type, $className);
  504. return str_replace('/', DS, $path) . Inflector::camelize($className) . 'Test.php';
  505. }
  506. /**
  507. * get the option parser.
  508. *
  509. * @return void
  510. */
  511. public function getOptionParser() {
  512. $parser = parent::getOptionParser();
  513. return $parser->description(__d('cake_console', 'Bake test case skeletons for classes.'))
  514. ->addArgument('type', array(
  515. 'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'),
  516. 'choices' => array(
  517. 'Controller', 'controller',
  518. 'Model', 'model',
  519. 'Helper', 'helper',
  520. 'Component', 'component',
  521. 'Behavior', 'behavior'
  522. )
  523. ))->addArgument('name', array(
  524. 'help' => __d('cake_console', 'An existing class to bake tests for.')
  525. ))->addOption('theme', array(
  526. 'short' => 't',
  527. 'help' => __d('cake_console', 'Theme to use when baking code.')
  528. ))->addOption('plugin', array(
  529. 'short' => 'p',
  530. 'help' => __d('cake_console', 'CamelCased name of the plugin to bake tests for.')
  531. ))->addOption('force', array(
  532. 'short' => 'f',
  533. 'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
  534. ))->epilog(__d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.'));
  535. }
  536. }