1: <?php
  2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16: 
 17: 
 18: App::uses('AppShell', 'Console/Command');
 19: App::uses('BakeTask', 'Console/Command/Task');
 20: App::uses('ClassRegistry', 'Utility');
 21: 
 22:  23:  24:  25:  26: 
 27: class TestTask extends BakeTask {
 28: 
 29:  30:  31:  32:  33: 
 34:     public $path = TESTS;
 35: 
 36:  37:  38:  39:  40: 
 41:     public $tasks = array('Template');
 42: 
 43:  44:  45:  46:  47: 
 48:     public $classTypes = array(
 49:         'Model' => 'Model',
 50:         'Controller' => 'Controller',
 51:         'Component' => 'Controller/Component',
 52:         'Behavior' => 'Model/Behavior',
 53:         'Helper' => 'View/Helper'
 54:     );
 55: 
 56:  57:  58:  59:  60:  61:  62: 
 63:     public $baseTypes = array(
 64:         'Model' => array('Model', 'Model'),
 65:         'Behavior' => array('ModelBehavior', 'Model'),
 66:         'Controller' => array('Controller', 'Controller'),
 67:         'Component' => array('Component', 'Controller'),
 68:         'Helper' => array('Helper', 'View')
 69:     );
 70: 
 71:  72:  73:  74:  75: 
 76:     protected $_fixtures = array();
 77: 
 78:  79:  80:  81:  82: 
 83:     public function execute() {
 84:         parent::execute();
 85:         $count = count($this->args);
 86:         if (!$count) {
 87:             $this->_interactive();
 88:         }
 89: 
 90:         if ($count === 1) {
 91:             $this->_interactive($this->args[0]);
 92:         }
 93: 
 94:         if ($count > 1) {
 95:             $type = Inflector::classify($this->args[0]);
 96:             if ($this->bake($type, $this->args[1])) {
 97:                 $this->out('<success>Done</success>');
 98:             }
 99:         }
100:     }
101: 
102: 103: 104: 105: 106: 107: 
108:     protected function _interactive($type = null) {
109:         $this->interactive = true;
110:         $this->hr();
111:         $this->out(__d('cake_console', 'Bake Tests'));
112:         $this->out(__d('cake_console', 'Path: %s', $this->getPath()));
113:         $this->hr();
114: 
115:         if ($type) {
116:             $type = Inflector::camelize($type);
117:             if (!isset($this->classTypes[$type])) {
118:                 $this->error(__d('cake_console', 'Incorrect type provided. Please choose one of %s', implode(', ', array_keys($this->classTypes))));
119:             }
120:         } else {
121:             $type = $this->getObjectType();
122:         }
123:         $className = $this->getClassName($type);
124:         return $this->bake($type, $className);
125:     }
126: 
127: 128: 129: 130: 131: 132: 133: 
134:     public function bake($type, $className) {
135:         $plugin = null;
136:         if ($this->plugin) {
137:             $plugin = $this->plugin . '.';
138:         }
139: 
140:         $realType = $this->mapType($type, $plugin);
141:         $fullClassName = $this->getRealClassName($type, $className);
142: 
143:         if ($this->typeCanDetectFixtures($type) && $this->isLoadableClass($realType, $fullClassName)) {
144:             $this->out(__d('cake_console', 'Bake is detecting possible fixtures...'));
145:             $testSubject = $this->buildTestSubject($type, $className);
146:             $this->generateFixtureList($testSubject);
147:         } elseif ($this->interactive) {
148:             $this->getUserFixtures();
149:         }
150:         list($baseClass, $baseType) = $this->getBaseType($type);
151:         App::uses($baseClass, $baseType);
152:         App::uses($fullClassName, $realType);
153: 
154:         $methods = array();
155:         if (class_exists($fullClassName)) {
156:             $methods = $this->getTestableMethods($fullClassName);
157:         }
158:         $mock = $this->hasMockClass($type, $fullClassName);
159:         list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName, $plugin);
160:         $uses = $this->generateUses($type, $realType, $fullClassName);
161: 
162:         $this->out("\n" . __d('cake_console', 'Baking test case for %s %s ...', $className, $type), 1, Shell::QUIET);
163: 
164:         $this->Template->set('fixtures', $this->_fixtures);
165:         $this->Template->set('plugin', $plugin);
166:         $this->Template->set(compact(
167:             'className', 'methods', 'type', 'fullClassName', 'mock',
168:             'realType', 'preConstruct', 'postConstruct', 'construction',
169:             'uses'
170:         ));
171:         $out = $this->Template->generate('classes', 'test');
172: 
173:         $filename = $this->testCaseFileName($type, $className);
174:         $made = $this->createFile($filename, $out);
175:         if ($made) {
176:             return $out;
177:         }
178:         return false;
179:     }
180: 
181: 182: 183: 184: 185: 
186:     public function getObjectType() {
187:         $this->hr();
188:         $this->out(__d('cake_console', 'Select an object type:'));
189:         $this->hr();
190: 
191:         $keys = array();
192:         $i = 0;
193:         foreach ($this->classTypes as $option => $package) {
194:             $this->out(++$i . '. ' . $option);
195:             $keys[] = $i;
196:         }
197:         $keys[] = 'q';
198:         $selection = $this->in(__d('cake_console', 'Enter the type of object to bake a test for or (q)uit'), $keys, 'q');
199:         if ($selection === 'q') {
200:             return $this->_stop();
201:         }
202:         $types = array_keys($this->classTypes);
203:         return $types[$selection - 1];
204:     }
205: 
206: 207: 208: 209: 210: 211: 
212:     public function getClassName($objectType) {
213:         $type = ucfirst(strtolower($objectType));
214:         $typeLength = strlen($type);
215:         $type = $this->classTypes[$type];
216:         if ($this->plugin) {
217:             $plugin = $this->plugin . '.';
218:             $options = App::objects($plugin . $type);
219:         } else {
220:             $options = App::objects($type);
221:         }
222:         $this->out(__d('cake_console', 'Choose a %s class', $objectType));
223:         $keys = array();
224:         foreach ($options as $key => $option) {
225:             $this->out(++$key . '. ' . $option);
226:             $keys[] = $key;
227:         }
228:         while (empty($selection)) {
229:             $selection = $this->in(__d('cake_console', 'Choose an existing class, or enter the name of a class that does not exist'));
230:             if (is_numeric($selection) && isset($options[$selection - 1])) {
231:                 $selection = $options[$selection - 1];
232:             }
233:             if ($type !== 'Model') {
234:                 $selection = substr($selection, 0, $typeLength * - 1);
235:             }
236:         }
237:         return $selection;
238:     }
239: 
240: 241: 242: 243: 244: 245: 246: 
247:     public function typeCanDetectFixtures($type) {
248:         $type = strtolower($type);
249:         return in_array($type, array('controller', 'model'));
250:     }
251: 
252: 253: 254: 255: 256: 257: 258: 
259:     public function isLoadableClass($package, $class) {
260:         App::uses($class, $package);
261:         list($plugin, $ns) = pluginSplit($package);
262:         if ($plugin) {
263:             App::uses("{$plugin}AppController", $package);
264:             App::uses("{$plugin}AppModel", $package);
265:             App::uses("{$plugin}AppHelper", $package);
266:         }
267:         return class_exists($class);
268:     }
269: 
270: 271: 272: 273: 274: 275: 276: 277: 
278:     public function buildTestSubject($type, $class) {
279:         ClassRegistry::flush();
280:         App::uses($class, $type);
281:         $class = $this->getRealClassName($type, $class);
282:         if (strtolower($type) === 'model') {
283:             $instance = ClassRegistry::init($class);
284:         } else {
285:             $instance = new $class();
286:         }
287:         return $instance;
288:     }
289: 
290: 291: 292: 293: 294: 295: 296: 297: 
298:     public function getRealClassName($type, $class) {
299:         if (strtolower($type) === 'model' || empty($this->classTypes[$type])) {
300:             return $class;
301:         }
302: 
303:         $position = strpos($class, $type);
304: 
305:         if ($position !== false && (strlen($class) - $position) === strlen($type)) {
306:             return $class;
307:         }
308:         return $class . $type;
309:     }
310: 
311: 312: 313: 314: 315: 316: 317: 318: 
319:     public function mapType($type, $plugin) {
320:         $type = ucfirst($type);
321:         if (empty($this->classTypes[$type])) {
322:             throw new CakeException(__d('cake_dev', 'Invalid object type.'));
323:         }
324:         $real = $this->classTypes[$type];
325:         if ($plugin) {
326:             $real = trim($plugin, '.') . '.' . $real;
327:         }
328:         return $real;
329:     }
330: 
331: 332: 333: 334: 335: 336: 337: 338: 
339:     public function getBaseType($type) {
340:         if (empty($this->baseTypes[$type])) {
341:             throw new CakeException(__d('cake_dev', 'Invalid type name'));
342:         }
343:         return $this->baseTypes[$type];
344:     }
345: 
346: 347: 348: 349: 350: 351: 352: 
353:     public function getTestableMethods($className) {
354:         $classMethods = get_class_methods($className);
355:         $parentMethods = get_class_methods(get_parent_class($className));
356:         $thisMethods = array_diff($classMethods, $parentMethods);
357:         $out = array();
358:         foreach ($thisMethods as $method) {
359:             if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) {
360:                 $out[] = $method;
361:             }
362:         }
363:         return $out;
364:     }
365: 
366: 367: 368: 369: 370: 371: 372: 
373:     public function generateFixtureList($subject) {
374:         $this->_fixtures = array();
375:         if ($subject instanceof Model) {
376:             $this->_processModel($subject);
377:         } elseif ($subject instanceof Controller) {
378:             $this->_processController($subject);
379:         }
380:         return array_values($this->_fixtures);
381:     }
382: 
383: 384: 385: 386: 387: 388: 389: 
390:     protected function _processModel($subject) {
391:         $this->_addFixture($subject->name);
392:         $associated = $subject->getAssociated();
393:         foreach ($associated as $alias => $type) {
394:             $className = $subject->{$alias}->name;
395:             if (!isset($this->_fixtures[$className])) {
396:                 $this->_processModel($subject->{$alias});
397:             }
398:             if ($type === 'hasAndBelongsToMany') {
399:                 if (!empty($subject->hasAndBelongsToMany[$alias]['with'])) {
400:                     list(, $joinModel) = pluginSplit($subject->hasAndBelongsToMany[$alias]['with']);
401:                 } else {
402:                     $joinModel = Inflector::classify($subject->hasAndBelongsToMany[$alias]['joinTable']);
403:                 }
404:                 if (!isset($this->_fixtures[$joinModel])) {
405:                     $this->_processModel($subject->{$joinModel});
406:                 }
407:             }
408:         }
409:     }
410: 
411: 412: 413: 414: 415: 416: 417: 
418:     protected function _processController($subject) {
419:         $subject->constructClasses();
420:         $models = array(Inflector::classify($subject->name));
421:         if (!empty($subject->uses)) {
422:             $models = $subject->uses;
423:         }
424:         foreach ($models as $model) {
425:             list(, $model) = pluginSplit($model);
426:             $this->_processModel($subject->{$model});
427:         }
428:     }
429: 
430: 431: 432: 433: 434: 435: 436: 
437:     protected function _addFixture($name) {
438:         if ($this->plugin) {
439:             $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.';
440:         } else {
441:             $prefix = 'app.';
442:         }
443:         $fixture = $prefix . Inflector::underscore($name);
444:         $this->_fixtures[$name] = $fixture;
445:     }
446: 
447: 448: 449: 450: 451: 
452:     public function getUserFixtures() {
453:         $proceed = $this->in(__d('cake_console', 'Bake could not detect fixtures, would you like to add some?'), array('y', 'n'), 'n');
454:         $fixtures = array();
455:         if (strtolower($proceed) === 'y') {
456:             $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'"));
457:             $fixtureListTrimmed = str_replace(' ', '', $fixtureList);
458:             $fixtures = explode(',', $fixtureListTrimmed);
459:         }
460:         $this->_fixtures = array_merge($this->_fixtures, $fixtures);
461:         return $fixtures;
462:     }
463: 
464: 465: 466: 467: 468: 469: 470: 
471:     public function hasMockClass($type) {
472:         $type = strtolower($type);
473:         return $type === 'controller';
474:     }
475: 
476: 477: 478: 479: 480: 481: 482: 483: 
484:     public function generateConstructor($type, $fullClassName, $plugin) {
485:         $type = strtolower($type);
486:         $pre = $construct = $post = '';
487:         if ($type === 'model') {
488:             $construct = "ClassRegistry::init('{$plugin}$fullClassName');\n";
489:         }
490:         if ($type === 'behavior') {
491:             $construct = "new $fullClassName();\n";
492:         }
493:         if ($type === 'helper') {
494:             $pre = "\$View = new View();\n";
495:             $construct = "new {$fullClassName}(\$View);\n";
496:         }
497:         if ($type === 'component') {
498:             $pre = "\$Collection = new ComponentCollection();\n";
499:             $construct = "new {$fullClassName}(\$Collection);\n";
500:         }
501:         return array($pre, $construct, $post);
502:     }
503: 
504: 505: 506: 507: 508: 509: 510: 511: 
512:     public function generateUses($type, $realType, $className) {
513:         $uses = array();
514:         $type = strtolower($type);
515:         if ($type === 'component') {
516:             $uses[] = array('ComponentCollection', 'Controller');
517:             $uses[] = array('Component', 'Controller');
518:         }
519:         if ($type === 'helper') {
520:             $uses[] = array('View', 'View');
521:             $uses[] = array('Helper', 'View');
522:         }
523:         $uses[] = array($className, $realType);
524:         return $uses;
525:     }
526: 
527: 528: 529: 530: 531: 532: 533: 534: 
535:     public function testCaseFileName($type, $className) {
536:         $path = $this->getPath() . 'Case' . DS;
537:         $type = Inflector::camelize($type);
538:         if (isset($this->classTypes[$type])) {
539:             $path .= $this->classTypes[$type] . DS;
540:         }
541:         $className = $this->getRealClassName($type, $className);
542:         return str_replace('/', DS, $path) . Inflector::camelize($className) . 'Test.php';
543:     }
544: 
545: 546: 547: 548: 549: 
550:     public function getOptionParser() {
551:         $parser = parent::getOptionParser();
552: 
553:         $parser->description(
554:             __d('cake_console', 'Bake test case skeletons for classes.')
555:         )->addArgument('type', array(
556:             'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'),
557:             'choices' => array(
558:                 'Controller', 'controller',
559:                 'Model', 'model',
560:                 'Helper', 'helper',
561:                 'Component', 'component',
562:                 'Behavior', 'behavior'
563:             )
564:         ))->addArgument('name', array(
565:             'help' => __d('cake_console', 'An existing class to bake tests for.')
566:         ))->addOption('theme', array(
567:             'short' => 't',
568:             'help' => __d('cake_console', 'Theme to use when baking code.')
569:         ))->addOption('plugin', array(
570:             'short' => 'p',
571:             'help' => __d('cake_console', 'CamelCased name of the plugin to bake tests for.')
572:         ))->addOption('force', array(
573:             'short' => 'f',
574:             'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
575:         ))->epilog(
576:             __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.')
577:         );
578: 
579:         return $parser;
580:     }
581: 
582: }
583: