source: Dev/branches/cakephp/cake/console/libs/tasks/test.php @ 126

Last change on this file since 126 was 126, checked in by fpvanagthoven, 14 years ago

Cakephp branch.

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