1<?php
2    /**
3     *	base include file for SimpleTest
4     *	@package	SimpleTest
5     *	@subpackage	UnitTester
6     *	@version	$Id: dumper.php,v 1.1 2005/11/09 23:41:18 gsmet Exp $
7     */
8    /**
9     * does type matter
10     */
11    define('TYPE_MATTERS', true);
12
13    /**
14     *    Displays variables as text and does diffs.
15	 *	  @package	SimpleTest
16	 *	  @subpackage	UnitTester
17     */
18    class SimpleDumper {
19
20        /**
21         *    Renders a variable in a shorter form than print_r().
22         *    @param mixed $value      Variable to render as a string.
23         *    @return string           Human readable string form.
24         *    @access public
25         */
26        function describeValue($value) {
27            $type = $this->getType($value);
28            switch($type) {
29                case "Null":
30                    return "NULL";
31                case "Boolean":
32                    return "Boolean: " . ($value ? "true" : "false");
33                case "Array":
34                    return "Array: " . count($value) . " items";
35                case "Object":
36                    return "Object: of " . get_class($value);
37                case "String":
38                    return "String: " . $this->clipString($value, 100);
39                default:
40                    return "$type: $value";
41            }
42            return "Unknown";
43        }
44
45        /**
46         *    Gets the string representation of a type.
47         *    @param mixed $value    Variable to check against.
48         *    @return string         Type.
49         *    @access public
50         */
51        function getType($value) {
52            if (! isset($value)) {
53                return "Null";
54            } elseif (is_bool($value)) {
55                return "Boolean";
56            } elseif (is_string($value)) {
57                return "String";
58            } elseif (is_integer($value)) {
59                return "Integer";
60            } elseif (is_float($value)) {
61                return "Float";
62            } elseif (is_array($value)) {
63                return "Array";
64            } elseif (is_resource($value)) {
65                return "Resource";
66            } elseif (is_object($value)) {
67                return "Object";
68            }
69            return "Unknown";
70        }
71
72        /**
73         *    Creates a human readable description of the
74         *    difference between two variables. Uses a
75         *    dynamic call.
76         *    @param mixed $first        First variable.
77         *    @param mixed $second       Value to compare with.
78         *    @param boolean $identical  If true then type anomolies count.
79         *    @return string             Description of difference.
80         *    @access public
81         */
82        function describeDifference($first, $second, $identical = false) {
83            if ($identical) {
84                if (! $this->_isTypeMatch($first, $second)) {
85                    return "with type mismatch as [" . $this->describeValue($first) .
86                        "] does not match [" . $this->describeValue($second) . "]";
87                }
88            }
89            $type = $this->getType($first);
90            if ($type == "Unknown") {
91                return "with unknown type";
92            }
93            $method = '_describe' . $type . 'Difference';
94            return $this->$method($first, $second, $identical);
95        }
96
97        /**
98         *    Tests to see if types match.
99         *    @param mixed $first        First variable.
100         *    @param mixed $second       Value to compare with.
101         *    @return boolean            True if matches.
102         *    @access private
103         */
104        function _isTypeMatch($first, $second) {
105            return ($this->getType($first) == $this->getType($second));
106        }
107
108        /**
109         *    Clips a string to a maximum length.
110         *    @param string $value         String to truncate.
111         *    @param integer $size         Minimum string size to show.
112         *    @param integer $position     Centre of string section.
113         *    @return string               Shortened version.
114         *    @access public
115         */
116        function clipString($value, $size, $position = 0) {
117            $length = strlen($value);
118            if ($length <= $size) {
119                return $value;
120            }
121            $position = min($position, $length);
122            $start = ($size/2 > $position ? 0 : $position - $size/2);
123            if ($start + $size > $length) {
124                $start = $length - $size;
125            }
126            $value = substr($value, $start, $size);
127            return ($start > 0 ? "..." : "") . $value . ($start + $size < $length ? "..." : "");
128        }
129
130        /**
131         *    Creates a human readable description of the
132         *    difference between two variables. The minimal
133         *    version.
134         *    @param null $first          First value.
135         *    @param mixed $second        Value to compare with.
136         *    @return string              Human readable description.
137         *    @access private
138         */
139        function _describeGenericDifference($first, $second) {
140            return "as [" . $this->describeValue($first) .
141                    "] does not match [" .
142                    $this->describeValue($second) . "]";
143        }
144
145        /**
146         *    Creates a human readable description of the
147         *    difference between a null and another variable.
148         *    @param null $first          First null.
149         *    @param mixed $second        Null to compare with.
150         *    @param boolean $identical   If true then type anomolies count.
151         *    @return string              Human readable description.
152         *    @access private
153         */
154        function _describeNullDifference($first, $second, $identical) {
155            return $this->_describeGenericDifference($first, $second);
156        }
157
158        /**
159         *    Creates a human readable description of the
160         *    difference between a boolean and another variable.
161         *    @param boolean $first       First boolean.
162         *    @param mixed $second        Boolean to compare with.
163         *    @param boolean $identical   If true then type anomolies count.
164         *    @return string              Human readable description.
165         *    @access private
166         */
167        function _describeBooleanDifference($first, $second, $identical) {
168            return $this->_describeGenericDifference($first, $second);
169        }
170
171        /**
172         *    Creates a human readable description of the
173         *    difference between a string and another variable.
174         *    @param string $first        First string.
175         *    @param mixed $second        String to compare with.
176         *    @param boolean $identical   If true then type anomolies count.
177         *    @return string              Human readable description.
178         *    @access private
179         */
180        function _describeStringDifference($first, $second, $identical) {
181            if (is_object($second) || is_array($second)) {
182                return $this->_describeGenericDifference($first, $second);
183            }
184            $position = $this->_stringDiffersAt($first, $second);
185            $message = "at character $position";
186            $message .= " with [" .
187                    $this->clipString($first, 100, $position) . "] and [" .
188                    $this->clipString($second, 100, $position) . "]";
189            return $message;
190        }
191
192        /**
193         *    Creates a human readable description of the
194         *    difference between an integer and another variable.
195         *    @param integer $first       First number.
196         *    @param mixed $second        Number to compare with.
197         *    @param boolean $identical   If true then type anomolies count.
198         *    @return string              Human readable description.
199         *    @access private
200         */
201        function _describeIntegerDifference($first, $second, $identical) {
202            if (is_object($second) || is_array($second)) {
203                return $this->_describeGenericDifference($first, $second);
204            }
205            return "because [" . $this->describeValue($first) .
206                    "] differs from [" .
207                    $this->describeValue($second) . "] by " .
208                    abs($first - $second);
209        }
210
211        /**
212         *    Creates a human readable description of the
213         *    difference between two floating point numbers.
214         *    @param float $first         First float.
215         *    @param mixed $second        Float to compare with.
216         *    @param boolean $identical   If true then type anomolies count.
217         *    @return string              Human readable description.
218         *    @access private
219         */
220        function _describeFloatDifference($first, $second, $identical) {
221            if (is_object($second) || is_array($second)) {
222                return $this->_describeGenericDifference($first, $second);
223            }
224            return "because " . $this->describeValue($first) .
225                    "] differs from [" .
226                    $this->describeValue($second) . "]";
227        }
228
229        /**
230         *    Creates a human readable description of the
231         *    difference between two arrays.
232         *    @param array $first         First array.
233         *    @param mixed $second        Array to compare with.
234         *    @param boolean $identical   If true then type anomolies count.
235         *    @return string              Human readable description.
236         *    @access private
237         */
238        function _describeArrayDifference($first, $second, $identical) {
239            if (! is_array($second)) {
240                return $this->_describeGenericDifference($first, $second);
241            }
242            if (! $this->_isMatchingKeys($first, $second, $identical)) {
243                return "as key list [" .
244                        implode(", ", array_keys($first)) . "] does not match key list [" .
245                        implode(", ", array_keys($second)) . "]";
246            }
247            foreach (array_keys($first) as $key) {
248                if ($identical && ($first[$key] === $second[$key])) {
249                    continue;
250                }
251                if (! $identical && ($first[$key] == $second[$key])) {
252                    continue;
253                }
254                return "with member [$key] " . $this->describeDifference(
255                        $first[$key],
256                        $second[$key],
257                        $identical);
258            }
259            return "";
260        }
261
262        /**
263         *    Compares two arrays to see if their key lists match.
264         *    For an identical match, the ordering and types of the keys
265         *    is significant.
266         *    @param array $first         First array.
267         *    @param array $second        Array to compare with.
268         *    @param boolean $identical   If true then type anomolies count.
269         *    @return boolean             True if matching.
270         *    @access private
271         */
272        function _isMatchingKeys($first, $second, $identical) {
273            $first_keys = array_keys($first);
274            $second_keys = array_keys($second);
275            if ($identical) {
276                return ($first_keys === $second_keys);
277            }
278            sort($first_keys);
279            sort($second_keys);
280            return ($first_keys == $second_keys);
281        }
282
283        /**
284         *    Creates a human readable description of the
285         *    difference between a resource and another variable.
286         *    @param resource $first       First resource.
287         *    @param mixed $second         Resource to compare with.
288         *    @param boolean $identical    If true then type anomolies count.
289         *    @return string              Human readable description.
290         *    @access private
291         */
292        function _describeResourceDifference($first, $second, $identical) {
293            return $this->_describeGenericDifference($first, $second);
294        }
295
296        /**
297         *    Creates a human readable description of the
298         *    difference between two objects.
299         *    @param object $first        First object.
300         *    @param mixed $second        Object to compare with.
301         *    @param boolean $identical   If true then type anomolies count.
302         *    @return string              Human readable description.
303         *    @access private
304         */
305        function _describeObjectDifference($first, $second, $identical) {
306            if (! is_object($second)) {
307                return $this->_describeGenericDifference($first, $second);
308            }
309            return $this->_describeArrayDifference(
310                    get_object_vars($first),
311                    get_object_vars($second),
312                    $identical);
313        }
314
315        /**
316         *    Find the first character position that differs
317         *    in two strings by binary chop.
318         *    @param string $first        First string.
319         *    @param string $second       String to compare with.
320         *    @return integer             Position of first differing
321         *                                character.
322         *    @access private
323         */
324        function _stringDiffersAt($first, $second) {
325            if (! $first || ! $second) {
326                return 0;
327            }
328            if (strlen($first) < strlen($second)) {
329                list($first, $second) = array($second, $first);
330            }
331            $position = 0;
332            $step = strlen($first);
333            while ($step > 1) {
334                $step = (integer)(($step + 1)/2);
335                if (strncmp($first, $second, $position + $step) == 0) {
336                    $position += $step;
337                }
338            }
339            return $position;
340        }
341
342        /**
343         *    Sends a formatted dump of a variable to a string.
344         *    @param mixed $variable    Variable to display.
345         *    @return string            Output from print_r().
346         *    @access public
347         *    @static
348         */
349        function dump($variable) {
350            ob_start();
351            print_r($variable);
352            $formatted = ob_get_contents();
353            ob_end_clean();
354            return $formatted;
355        }
356
357        /**
358         *    Extracts the last assertion that was not within
359         *    Simpletest itself. The name must start with "assert".
360         *    @param array $stack      List of stack frames.
361         *    @param string $format    String formatting.
362         *    @param string $prefix    Prefix of method to search for.
363         *    @access public
364         *    @static
365         */
366        function getFormattedAssertionLine($stack, $format = '%d', $prefix = 'assert') {
367            foreach ($stack as $frame) {
368                if (isset($frame['file']) && strpos($frame['file'], 'simpletest') !== false) {     // dirname() is a bit slow.
369                    if (substr(dirname($frame['file']), -10) == 'simpletest') {
370                        continue;
371                    }
372                }
373                if (strncmp($frame['function'], $prefix, strlen($prefix)) == 0) {
374                    return sprintf($format, $frame['line']);
375                }
376            }
377            return '';
378        }
379    }
380?>