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 | |
---|
21 | include_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 | */ |
---|
29 | class 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 | } |
---|