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('&amp;', '&lt;', '&gt;', '&quot;', '&apos;'),
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