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: return $parser->description(__d('cake_console', 'Bake test case skeletons for classes.'))
553: ->addArgument('type', array(
554: 'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'),
555: 'choices' => array(
556: 'Controller', 'controller',
557: 'Model', 'model',
558: 'Helper', 'helper',
559: 'Component', 'component',
560: 'Behavior', 'behavior'
561: )
562: ))->addArgument('name', array(
563: 'help' => __d('cake_console', 'An existing class to bake tests for.')
564: ))->addOption('theme', array(
565: 'short' => 't',
566: 'help' => __d('cake_console', 'Theme to use when baking code.')
567: ))->addOption('plugin', array(
568: 'short' => 'p',
569: 'help' => __d('cake_console', 'CamelCased name of the plugin to bake tests for.')
570: ))->addOption('force', array(
571: 'short' => 'f',
572: 'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
573: ))->epilog(__d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.'));
574: }
575:
576: }
577: