1<?php 2/** 3 * base include file for SimpleTest 4 * @package SimpleTest 5 * @subpackage UnitTester 6 * @version $Id$ 7 */ 8 9/**#@+ 10 * include other SimpleTest class files 11 */ 12require_once(dirname(__FILE__) . '/scorer.php'); 13/**#@-*/ 14 15/** 16 * Creates the XML needed for remote communication 17 * by SimpleTest. 18 * @package SimpleTest 19 * @subpackage UnitTester 20 */ 21class XmlReporter extends SimpleReporter { 22 private $indent; 23 private $namespace; 24 25 /** 26 * Sets up indentation and namespace. 27 * @param string $namespace Namespace to add to each tag. 28 * @param string $indent Indenting to add on each nesting. 29 * @access public 30 */ 31 function __construct($namespace = false, $indent = ' ') { 32 parent::__construct(); 33 $this->namespace = ($namespace ? $namespace . ':' : ''); 34 $this->indent = $indent; 35 } 36 37 /** 38 * Calculates the pretty printing indent level 39 * from the current level of nesting. 40 * @param integer $offset Extra indenting level. 41 * @return string Leading space. 42 * @access protected 43 */ 44 protected function getIndent($offset = 0) { 45 return str_repeat( 46 $this->indent, 47 count($this->getTestList()) + $offset); 48 } 49 50 /** 51 * Converts character string to parsed XML 52 * entities string. 53 * @param string text Unparsed character data. 54 * @return string Parsed character data. 55 * @access public 56 */ 57 function toParsedXml($text) { 58 return str_replace( 59 array('&', '<', '>', '"', '\''), 60 array('&', '<', '>', '"', '''), 61 $text); 62 } 63 64 /** 65 * Paints the start of a group test. 66 * @param string $test_name Name of test that is starting. 67 * @param integer $size Number of test cases starting. 68 * @access public 69 */ 70 function paintGroupStart($test_name, $size) { 71 parent::paintGroupStart($test_name, $size); 72 print $this->getIndent(); 73 print "<" . $this->namespace . "group size=\"$size\">\n"; 74 print $this->getIndent(1); 75 print "<" . $this->namespace . "name>" . 76 $this->toParsedXml($test_name) . 77 "</" . $this->namespace . "name>\n"; 78 } 79 80 /** 81 * Paints the end of a group test. 82 * @param string $test_name Name of test that is ending. 83 * @access public 84 */ 85 function paintGroupEnd($test_name) { 86 print $this->getIndent(); 87 print "</" . $this->namespace . "group>\n"; 88 parent::paintGroupEnd($test_name); 89 } 90 91 /** 92 * Paints the start of a test case. 93 * @param string $test_name Name of test that is starting. 94 * @access public 95 */ 96 function paintCaseStart($test_name) { 97 parent::paintCaseStart($test_name); 98 print $this->getIndent(); 99 print "<" . $this->namespace . "case>\n"; 100 print $this->getIndent(1); 101 print "<" . $this->namespace . "name>" . 102 $this->toParsedXml($test_name) . 103 "</" . $this->namespace . "name>\n"; 104 } 105 106 /** 107 * Paints the end of a test case. 108 * @param string $test_name Name of test that is ending. 109 * @access public 110 */ 111 function paintCaseEnd($test_name) { 112 print $this->getIndent(); 113 print "</" . $this->namespace . "case>\n"; 114 parent::paintCaseEnd($test_name); 115 } 116 117 /** 118 * Paints the start of a test method. 119 * @param string $test_name Name of test that is starting. 120 * @access public 121 */ 122 function paintMethodStart($test_name) { 123 parent::paintMethodStart($test_name); 124 print $this->getIndent(); 125 print "<" . $this->namespace . "test>\n"; 126 print $this->getIndent(1); 127 print "<" . $this->namespace . "name>" . 128 $this->toParsedXml($test_name) . 129 "</" . $this->namespace . "name>\n"; 130 } 131 132 /** 133 * Paints the end of a test method. 134 * @param string $test_name Name of test that is ending. 135 * @param integer $progress Number of test cases ending. 136 * @access public 137 */ 138 function paintMethodEnd($test_name) { 139 print $this->getIndent(); 140 print "</" . $this->namespace . "test>\n"; 141 parent::paintMethodEnd($test_name); 142 } 143 144 /** 145 * Paints pass as XML. 146 * @param string $message Message to encode. 147 * @access public 148 */ 149 function paintPass($message) { 150 parent::paintPass($message); 151 print $this->getIndent(1); 152 print "<" . $this->namespace . "pass>"; 153 print $this->toParsedXml($message); 154 print "</" . $this->namespace . "pass>\n"; 155 } 156 157 /** 158 * Paints failure as XML. 159 * @param string $message Message to encode. 160 * @access public 161 */ 162 function paintFail($message) { 163 parent::paintFail($message); 164 print $this->getIndent(1); 165 print "<" . $this->namespace . "fail>"; 166 print $this->toParsedXml($message); 167 print "</" . $this->namespace . "fail>\n"; 168 } 169 170 /** 171 * Paints error as XML. 172 * @param string $message Message to encode. 173 * @access public 174 */ 175 function paintError($message) { 176 parent::paintError($message); 177 print $this->getIndent(1); 178 print "<" . $this->namespace . "exception>"; 179 print $this->toParsedXml($message); 180 print "</" . $this->namespace . "exception>\n"; 181 } 182 183 /** 184 * Paints exception as XML. 185 * @param Exception $exception Exception to encode. 186 * @access public 187 */ 188 function paintException($exception) { 189 parent::paintException($exception); 190 print $this->getIndent(1); 191 print "<" . $this->namespace . "exception>"; 192 $message = 'Unexpected exception of type [' . get_class($exception) . 193 '] with message ['. $exception->getMessage() . 194 '] in ['. $exception->getFile() . 195 ' line ' . $exception->getLine() . ']'; 196 print $this->toParsedXml($message); 197 print "</" . $this->namespace . "exception>\n"; 198 } 199 200 /** 201 * Paints the skipping message and tag. 202 * @param string $message Text to display in skip tag. 203 * @access public 204 */ 205 function paintSkip($message) { 206 parent::paintSkip($message); 207 print $this->getIndent(1); 208 print "<" . $this->namespace . "skip>"; 209 print $this->toParsedXml($message); 210 print "</" . $this->namespace . "skip>\n"; 211 } 212 213 /** 214 * Paints a simple supplementary message. 215 * @param string $message Text to display. 216 * @access public 217 */ 218 function paintMessage($message) { 219 parent::paintMessage($message); 220 print $this->getIndent(1); 221 print "<" . $this->namespace . "message>"; 222 print $this->toParsedXml($message); 223 print "</" . $this->namespace . "message>\n"; 224 } 225 226 /** 227 * Paints a formatted ASCII message such as a 228 * privateiable dump. 229 * @param string $message Text to display. 230 * @access public 231 */ 232 function paintFormattedMessage($message) { 233 parent::paintFormattedMessage($message); 234 print $this->getIndent(1); 235 print "<" . $this->namespace . "formatted>"; 236 print "<![CDATA[$message]]>"; 237 print "</" . $this->namespace . "formatted>\n"; 238 } 239 240 /** 241 * Serialises the event object. 242 * @param string $type Event type as text. 243 * @param mixed $payload Message or object. 244 * @access public 245 */ 246 function paintSignal($type, $payload) { 247 parent::paintSignal($type, $payload); 248 print $this->getIndent(1); 249 print "<" . $this->namespace . "signal type=\"$type\">"; 250 print "<![CDATA[" . serialize($payload) . "]]>"; 251 print "</" . $this->namespace . "signal>\n"; 252 } 253 254 /** 255 * Paints the test document header. 256 * @param string $test_name First test top level 257 * to start. 258 * @access public 259 * @abstract 260 */ 261 function paintHeader($test_name) { 262 if (! SimpleReporter::inCli()) { 263 header('Content-type: text/xml'); 264 } 265 print "<?xml version=\"1.0\""; 266 if ($this->namespace) { 267 print " xmlns:" . $this->namespace . 268 "=\"www.lastcraft.com/SimpleTest/Beta3/Report\""; 269 } 270 print "?>\n"; 271 print "<" . $this->namespace . "run>\n"; 272 } 273 274 /** 275 * Paints the test document footer. 276 * @param string $test_name The top level test. 277 * @access public 278 * @abstract 279 */ 280 function paintFooter($test_name) { 281 print "</" . $this->namespace . "run>\n"; 282 } 283} 284 285/** 286 * Accumulator for incoming tag. Holds the 287 * incoming test structure information for 288 * later dispatch to the reporter. 289 * @package SimpleTest 290 * @subpackage UnitTester 291 */ 292class NestingXmlTag { 293 private $name; 294 private $attributes; 295 296 /** 297 * Sets the basic test information except 298 * the name. 299 * @param hash $attributes Name value pairs. 300 * @access public 301 */ 302 function NestingXmlTag($attributes) { 303 $this->name = false; 304 $this->attributes = $attributes; 305 } 306 307 /** 308 * Sets the test case/method name. 309 * @param string $name Name of test. 310 * @access public 311 */ 312 function setName($name) { 313 $this->name = $name; 314 } 315 316 /** 317 * Accessor for name. 318 * @return string Name of test. 319 * @access public 320 */ 321 function getName() { 322 return $this->name; 323 } 324 325 /** 326 * Accessor for attributes. 327 * @return hash All attributes. 328 * @access protected 329 */ 330 protected function getAttributes() { 331 return $this->attributes; 332 } 333} 334 335/** 336 * Accumulator for incoming method tag. Holds the 337 * incoming test structure information for 338 * later dispatch to the reporter. 339 * @package SimpleTest 340 * @subpackage UnitTester 341 */ 342class NestingMethodTag extends NestingXmlTag { 343 344 /** 345 * Sets the basic test information except 346 * the name. 347 * @param hash $attributes Name value pairs. 348 * @access public 349 */ 350 function NestingMethodTag($attributes) { 351 $this->NestingXmlTag($attributes); 352 } 353 354 /** 355 * Signals the appropriate start event on the 356 * listener. 357 * @param SimpleReporter $listener Target for events. 358 * @access public 359 */ 360 function paintStart(&$listener) { 361 $listener->paintMethodStart($this->getName()); 362 } 363 364 /** 365 * Signals the appropriate end event on the 366 * listener. 367 * @param SimpleReporter $listener Target for events. 368 * @access public 369 */ 370 function paintEnd(&$listener) { 371 $listener->paintMethodEnd($this->getName()); 372 } 373} 374 375/** 376 * Accumulator for incoming case tag. Holds the 377 * incoming test structure information for 378 * later dispatch to the reporter. 379 * @package SimpleTest 380 * @subpackage UnitTester 381 */ 382class NestingCaseTag extends NestingXmlTag { 383 384 /** 385 * Sets the basic test information except 386 * the name. 387 * @param hash $attributes Name value pairs. 388 * @access public 389 */ 390 function NestingCaseTag($attributes) { 391 $this->NestingXmlTag($attributes); 392 } 393 394 /** 395 * Signals the appropriate start event on the 396 * listener. 397 * @param SimpleReporter $listener Target for events. 398 * @access public 399 */ 400 function paintStart(&$listener) { 401 $listener->paintCaseStart($this->getName()); 402 } 403 404 /** 405 * Signals the appropriate end event on the 406 * listener. 407 * @param SimpleReporter $listener Target for events. 408 * @access public 409 */ 410 function paintEnd(&$listener) { 411 $listener->paintCaseEnd($this->getName()); 412 } 413} 414 415/** 416 * Accumulator for incoming group tag. Holds the 417 * incoming test structure information for 418 * later dispatch to the reporter. 419 * @package SimpleTest 420 * @subpackage UnitTester 421 */ 422class NestingGroupTag extends NestingXmlTag { 423 424 /** 425 * Sets the basic test information except 426 * the name. 427 * @param hash $attributes Name value pairs. 428 * @access public 429 */ 430 function NestingGroupTag($attributes) { 431 $this->NestingXmlTag($attributes); 432 } 433 434 /** 435 * Signals the appropriate start event on the 436 * listener. 437 * @param SimpleReporter $listener Target for events. 438 * @access public 439 */ 440 function paintStart(&$listener) { 441 $listener->paintGroupStart($this->getName(), $this->getSize()); 442 } 443 444 /** 445 * Signals the appropriate end event on the 446 * listener. 447 * @param SimpleReporter $listener Target for events. 448 * @access public 449 */ 450 function paintEnd(&$listener) { 451 $listener->paintGroupEnd($this->getName()); 452 } 453 454 /** 455 * The size in the attributes. 456 * @return integer Value of size attribute or zero. 457 * @access public 458 */ 459 function getSize() { 460 $attributes = $this->getAttributes(); 461 if (isset($attributes['SIZE'])) { 462 return (integer)$attributes['SIZE']; 463 } 464 return 0; 465 } 466} 467 468/** 469 * Parser for importing the output of the XmlReporter. 470 * Dispatches that output to another reporter. 471 * @package SimpleTest 472 * @subpackage UnitTester 473 */ 474class SimpleTestXmlParser { 475 private $listener; 476 private $expat; 477 private $tag_stack; 478 private $in_content_tag; 479 private $content; 480 private $attributes; 481 482 /** 483 * Loads a listener with the SimpleReporter 484 * interface. 485 * @param SimpleReporter $listener Listener of tag events. 486 * @access public 487 */ 488 function SimpleTestXmlParser(&$listener) { 489 $this->listener = &$listener; 490 $this->expat = &$this->createParser(); 491 $this->tag_stack = array(); 492 $this->in_content_tag = false; 493 $this->content = ''; 494 $this->attributes = array(); 495 } 496 497 /** 498 * Parses a block of XML sending the results to 499 * the listener. 500 * @param string $chunk Block of text to read. 501 * @return boolean True if valid XML. 502 * @access public 503 */ 504 function parse($chunk) { 505 if (! xml_parse($this->expat, $chunk)) { 506 trigger_error('XML parse error with ' . 507 xml_error_string(xml_get_error_code($this->expat))); 508 return false; 509 } 510 return true; 511 } 512 513 /** 514 * Sets up expat as the XML parser. 515 * @return resource Expat handle. 516 * @access protected 517 */ 518 protected function &createParser() { 519 $expat = xml_parser_create(); 520 xml_set_object($expat, $this); 521 xml_set_element_handler($expat, 'startElement', 'endElement'); 522 xml_set_character_data_handler($expat, 'addContent'); 523 xml_set_default_handler($expat, 'defaultContent'); 524 return $expat; 525 } 526 527 /** 528 * Opens a new test nesting level. 529 * @return NestedXmlTag The group, case or method tag 530 * to start. 531 * @access private 532 */ 533 protected function pushNestingTag($nested) { 534 array_unshift($this->tag_stack, $nested); 535 } 536 537 /** 538 * Accessor for current test structure tag. 539 * @return NestedXmlTag The group, case or method tag 540 * being parsed. 541 * @access private 542 */ 543 protected function &getCurrentNestingTag() { 544 return $this->tag_stack[0]; 545 } 546 547 /** 548 * Ends a nesting tag. 549 * @return NestedXmlTag The group, case or method tag 550 * just finished. 551 * @access private 552 */ 553 protected function popNestingTag() { 554 return array_shift($this->tag_stack); 555 } 556 557 /** 558 * Test if tag is a leaf node with only text content. 559 * @param string $tag XML tag name. 560 * @return @boolean True if leaf, false if nesting. 561 * @private 562 */ 563 protected function isLeaf($tag) { 564 return in_array($tag, array( 565 'NAME', 'PASS', 'FAIL', 'EXCEPTION', 'SKIP', 'MESSAGE', 'FORMATTED', 'SIGNAL')); 566 } 567 568 /** 569 * Handler for start of event element. 570 * @param resource $expat Parser handle. 571 * @param string $tag Element name. 572 * @param hash $attributes Name value pairs. 573 * Attributes without content 574 * are marked as true. 575 * @access protected 576 */ 577 protected function startElement($expat, $tag, $attributes) { 578 $this->attributes = $attributes; 579 if ($tag == 'GROUP') { 580 $this->pushNestingTag(new NestingGroupTag($attributes)); 581 } elseif ($tag == 'CASE') { 582 $this->pushNestingTag(new NestingCaseTag($attributes)); 583 } elseif ($tag == 'TEST') { 584 $this->pushNestingTag(new NestingMethodTag($attributes)); 585 } elseif ($this->isLeaf($tag)) { 586 $this->in_content_tag = true; 587 $this->content = ''; 588 } 589 } 590 591 /** 592 * End of element event. 593 * @param resource $expat Parser handle. 594 * @param string $tag Element name. 595 * @access protected 596 */ 597 protected function endElement($expat, $tag) { 598 $this->in_content_tag = false; 599 if (in_array($tag, array('GROUP', 'CASE', 'TEST'))) { 600 $nesting_tag = $this->popNestingTag(); 601 $nesting_tag->paintEnd($this->listener); 602 } elseif ($tag == 'NAME') { 603 $nesting_tag = &$this->getCurrentNestingTag(); 604 $nesting_tag->setName($this->content); 605 $nesting_tag->paintStart($this->listener); 606 } elseif ($tag == 'PASS') { 607 $this->listener->paintPass($this->content); 608 } elseif ($tag == 'FAIL') { 609 $this->listener->paintFail($this->content); 610 } elseif ($tag == 'EXCEPTION') { 611 $this->listener->paintError($this->content); 612 } elseif ($tag == 'SKIP') { 613 $this->listener->paintSkip($this->content); 614 } elseif ($tag == 'SIGNAL') { 615 $this->listener->paintSignal( 616 $this->attributes['TYPE'], 617 unserialize($this->content)); 618 } elseif ($tag == 'MESSAGE') { 619 $this->listener->paintMessage($this->content); 620 } elseif ($tag == 'FORMATTED') { 621 $this->listener->paintFormattedMessage($this->content); 622 } 623 } 624 625 /** 626 * Content between start and end elements. 627 * @param resource $expat Parser handle. 628 * @param string $text Usually output messages. 629 * @access protected 630 */ 631 protected function addContent($expat, $text) { 632 if ($this->in_content_tag) { 633 $this->content .= $text; 634 } 635 return true; 636 } 637 638 /** 639 * XML and Doctype handler. Discards all such content. 640 * @param resource $expat Parser handle. 641 * @param string $default Text of default content. 642 * @access protected 643 */ 644 protected function defaultContent($expat, $default) { 645 } 646} 647?> 648