lime.php
1 <?php
2
/**
3  * Unit test library v1.4. based on  Lime
4  *
5  * @author Zhou Yuan <yuanzhou19@gmail.com>
6  * @link http://www.infopotato.com/
7  * @copyright Copyright &copy; 2009-2011 Zhou Yuan
8  * @license http://www.opensource.org/licenses/mit-license.php MIT Licence 
9  */
10
class lime_test {
11     const 
EPSILON 0.0000000001;
12
13     protected 
$test_nb 0;
14     protected 
$output  NULL;
15     protected 
$results = array();
16     protected 
$options = array();
17
18     static protected 
$all_results = array();
19
20     public function 
__construct($plan NULL$options = array()) {
21         
$this->options array_merge(array(
22             
'force_colors'    => FALSE,
23             
'output'          => NULL,
24             
'verbose'         => FALSE,
25             
'error_reporting' => FALSE,
26         ), 
$options);
27
28         
$this->output $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']);
29
30         
$caller $this->find_caller(debug_backtrace());
31         
self::$all_results[] = array(
32             
'file'  => $caller[0],
33             
'tests' => array(),
34             
'stats' => array('plan' => $plan'total' => 0'failed' => array(), 'passed' => array(), 'skipped' => array(), 'errors' => array()),
35         );
36
37         
$this->results = &self::$all_results[count(self::$all_results) - 1];
38
39         
NULL !== $plan and $this->output->echoln(sprintf("1..%d"$plan));
40
41         
set_error_handler(array($this'handle_error'));
42         
set_exception_handler(array($this'handle_exception'));
43     }
44
45     static public function 
reset() {
46         
self::$all_results = array();
47     }
48
49     public function 
__destruct() {
50         
$plan $this->results['stats']['plan'];
51         
$passed count($this->results['stats']['passed']);
52         
$failed count($this->results['stats']['failed']);
53         
$total $this->results['stats']['total'];
54         
is_null($plan) and $plan $total and $this->output->echoln(sprintf("1..%d"$plan));
55
56         if (
$total $plan) {
57             
$this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra."$plan$total $plan));
58         } elseif (
$total $plan) {
59             
$this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d."$plan$total));
60         }
61
62         if (
$failed) {
63             
$this->output->red_bar(sprintf("# Looks like you failed %d tests of %d."$failed$passed $failed));
64         } elseif (
$total == $plan) {
65             
$this->output->green_bar("# Looks like everything went fine.");
66         }
67
68          
flush();
69     }
70
71     
/**
72      * Tests a condition and passes if it is true
73      *
74      * @param mixed  $exp     condition to test
75      * @param string $message display output message when the test passes
76      *
77      * @return boolean
78      */
79     
public function ok($exp$message '') {
80         
$this->update_stats();
81
82         if (
$result = (boolean) $exp) {
83             
$this->results['stats']['passed'][] = $this->test_nb;
84         } else {
85             
$this->results['stats']['failed'][] = $this->test_nb;
86         }
87         
$this->results['tests'][$this->test_nb]['message'] = $message;
88         
$this->results['tests'][$this->test_nb]['status'] = $result;
89         
$this->output->echoln(sprintf("%s %d%s"$result 'ok' 'not ok'$this->test_nb$message $message sprintf('%s %s'=== strpos($message'#') ? '' ' -'$message) : ''));
90
91         if ( ! 
$result) {
92             
$this->output->diag(sprintf('    Failed test (%s at line %d)'str_replace(getcwd(), '.'$this->results['tests'][$this->test_nb]['file']), $this->results['tests'][$this->test_nb]['line']));
93         }
94
95         return 
$result;
96     }
97
98     
/**
99      * Compares two values and passes if they are equal (==)
100      *
101      * @param mixed  $exp1    left value
102      * @param mixed  $exp2    right value
103      * @param string $message display output message when the test passes
104      *
105      * @return boolean
106      */
107     
public function is($exp1$exp2$message '') {
108         if (
is_object($exp1) || is_object($exp2)) {
109             
$value $exp1 === $exp2;
110         } else if (
is_float($exp1) && is_float($exp2)) {
111             
$value abs($exp1 $exp2) < self::EPSILON;
112         } else {
113             
$value $exp1 == $exp2;
114         }
115
116         if ( ! 
$result $this->ok($value$message)) {
117             
$this->set_last_test_errors(array(sprintf("           got: %s"var_export($exp1TRUE)), sprintf("      expected: %s"var_export($exp2TRUE))));
118         }
119
120         return 
$result;
121     }
122
123     
/**
124      * Compares two values and passes if they are not equal
125      *
126      * @param mixed  $exp1    left value
127      * @param mixed  $exp2    right value
128      * @param string $message display output message when the test passes
129      *
130      * @return boolean
131      */
132     
public function isnt($exp1$exp2$message '') {
133         if ( ! 
$result $this->ok($exp1 != $exp2$message)) {
134             
$this->set_last_test_errors(array(sprintf("      %s"var_export($exp2TRUE)), '          ne'sprintf("      %s"var_export($exp2TRUE))));
135         }
136
137         return 
$result;
138     }
139
140     
/**
141      * Tests a string against a regular expression
142      *
143      * @param string $exp     value to test
144      * @param string $regex   the pattern to search for, as a string
145      * @param string $message display output message when the test passes
146      *
147      * @return boolean
148      */
149     
public function like($exp$regex$message '') {
150         if ( ! 
$result $this->ok(preg_match($regex$exp), $message)) {
151             
$this->set_last_test_errors(array(sprintf("                    '%s'"$exp), sprintf("      doesn't match '%s'"$regex)));
152         }
153
154         return 
$result;
155     }
156
157     
/**
158      * Checks that a string doesn't match a regular expression
159      *
160      * @param string $exp     value to test
161      * @param string $regex   the pattern to search for, as a string
162      * @param string $message display output message when the test passes
163      *
164      * @return boolean
165      */
166     
public function unlike($exp$regex$message '') {
167         if (!
$result $this->ok(!preg_match($regex$exp), $message)) {
168             
$this->set_last_test_errors(array(sprintf("               '%s'"$exp), sprintf("      matches '%s'"$regex)));
169         }
170
171         return 
$result;
172     }
173
174     
/**
175      * Compares two arguments with an operator
176      *
177      * @param mixed  $exp1    left value
178      * @param string $op      operator
179      * @param mixed  $exp2    right value
180      * @param string $message display output message when the test passes
181      *
182      * @return boolean
183      */
184     
public function cmp_ok($exp1$op$exp2$message '') {
185         
$php sprintf("\$result = \$exp1 $op \$exp2;");
186         
// under some unknown conditions the sprintf() call causes a segmentation fault
187         // when placed directly in the eval() call
188         
eval($php);
189
190         if ( ! 
$this->ok($result$message)) {
191             
$this->set_last_test_errors(array(sprintf("      %s"str_replace("\n"''var_export($exp1TRUE))), sprintf("          %s"$op), sprintf("      %s"str_replace("\n"''var_export($exp2TRUE)))));
192         }
193
194         return 
$result;
195     }
196
197     
/**
198      * Checks the availability of a method for an object or a class
199      *
200      * @param mixed        $object  an object instance or a class name
201      * @param string|array $methods one or more method names
202      * @param string       $message display output message when the test passes
203      *
204      * @return boolean
205      */
206     
public function can_ok($object$methods$message '') {
207         
$result TRUE;
208         
$failed_messages = array();
209         foreach ((array) 
$methods as $method) {
210             if ( ! 
method_exists($object$method)) {
211                 
$failed_messages[] = sprintf("      method '%s' does not exist"$method);
212                 
$result FALSE;
213             }
214         }
215
216         !
$this->ok($result$message);
217
218         !
$result and $this->set_last_test_errors($failed_messages);
219
220         return 
$result;
221     }
222
223     
/**
224      * Checks the type of an argument
225      *
226      * @param mixed  $var     variable instance
227      * @param string $class   class or type name
228      * @param string $message display output message when the test passes
229      *
230      * @return boolean
231      */
232     
public function isa_ok($var$class$message '') {
233         
$type is_object($var) ? get_class($var) : gettype($var);
234         if ( ! 
$result $this->ok($type == $class$message)) {
235             
$this->set_last_test_errors(array(sprintf("      variable isn't a '%s' it's a '%s'"$class$type)));
236         }
237
238         return 
$result;
239     }
240
241     
/**
242      * Checks that two arrays have the same values
243      *
244      * @param mixed  $exp1    first variable
245      * @param mixed  $exp2    second variable
246      * @param string $message display output message when the test passes
247      *
248      * @return boolean
249      */
250     
public function is_deeply($exp1$exp2$message '') {
251         if ( ! 
$result $this->ok($this->_test_is_deeply($exp1$exp2), $message)) {
252             
$this->set_last_test_errors(array(sprintf("           got: %s"str_replace("\n"''var_export($exp1TRUE))), sprintf("      expected: %s"str_replace("\n"''var_export($exp2TRUE)))));
253         }
254
255         return 
$result;
256     }
257
258     
/**
259      * Always passes--useful for testing exceptions
260      *
261      * @param string $message display output message
262      *
263      * @return TRUE
264      */
265     
public function pass($message '') {
266         return 
$this->ok(TRUE$message);
267     }
268
269     
/**
270      * Always fails--useful for testing exceptions
271      *
272      * @param string $message display output message
273      *
274      * @return FALSE
275      */
276     
public function fail($message '') {
277         return 
$this->ok(FALSE$message);
278     }
279
280     
/**
281      * Outputs a diag message but runs no test
282      *
283      * @param string $message display output message
284      *
285      * @return void
286      */
287     
public function diag($message) {
288         
$this->output->diag($message);
289     }
290
291     
/**
292      * Counts as $nb_tests tests--useful for conditional tests
293      *
294      * @param string  $message  display output message
295      * @param integer $nb_tests number of tests to skip
296      *
297      * @return void
298      */
299     
public function skip($message ''$nb_tests 1) {
300         for (
$i 0$i $nb_tests$i++) {
301             
$this->pass(sprintf("# SKIP%s"$message ' '.$message ''));
302             
$this->results['stats']['skipped'][] = $this->test_nb;
303             
array_pop($this->results['stats']['passed']);
304         }
305     }
306
307     
/**
308      * Counts as a test--useful for tests yet to be written
309      *
310      * @param string $message display output message
311      *
312      * @return void
313      */
314     
public function todo($message '') {
315         
$this->pass(sprintf("# TODO%s"$message ' '.$message ''));
316         
$this->results['stats']['skipped'][] = $this->test_nb;
317         
array_pop($this->results['stats']['passed']);
318     }
319
320     
/**
321      * Validates that a file exists and that it is properly included
322      *
323      * @param string $file    file path
324      * @param string $message display output message when the test passes
325      *
326      * @return boolean
327      */
328     
public function include_ok($file$message '') {
329         if ( ! 
$result $this->ok((@include($file)) == 1$message)) {
330             
$this->set_last_test_errors(array(sprintf("      Tried to include '%s'"$file)));
331         }
332
333         return 
$result;
334     }
335
336     private function 
_test_is_deeply($var1$var2) {
337         if (
gettype($var1) != gettype($var2)) {
338             return 
FALSE;
339         }
340
341         if (
is_array($var1)) {
342             
ksort($var1);
343             
ksort($var2);
344
345             
$keys1 array_keys($var1);
346             
$keys2 array_keys($var2);
347             if (
array_diff($keys1$keys2) || array_diff($keys2$keys1)) {
348                 return 
FALSE;
349             }
350             
$is_equal TRUE;
351             foreach (
$var1 as $key => $value) {
352                 
$is_equal $this->_test_is_deeply($var1[$key], $var2[$key]);
353                 if (
$is_equal === FALSE) {
354                      break;
355                 }
356             }
357
358             return 
$is_equal;
359         } else {
360             return 
$var1 === $var2;
361         }
362     }
363
364     public function 
comment($message) {
365         
$this->output->comment($message);
366     }
367
368     public function 
info($message) {
369         
$this->output->info($message);
370     }
371
372     public function 
error($message$file NULL$line NULL$traces = array()) {
373         
$this->output->error($message$file$line$traces);
374
375           
$this->results['stats']['errors'][] = array(
376               
'message' => $message,
377               
'file' => $file,
378               
'line' => $line,
379           );
380     }
381
382     protected function 
update_stats() {
383         ++
$this->test_nb;
384         ++
$this->results['stats']['total'];
385
386         list(
$this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace());
387     }
388
389     protected function 
set_last_test_errors($errors = array()) {
390         
$this->output->diag($errors);
391
392         
$this->results['tests'][$this->test_nb]['error'] = implode("\n"$errors);
393     }
394
395     protected function 
find_caller($traces) {
396         
// find the first call to a method of an object that is an instance of lime_test
397         
$t array_reverse($traces);
398         foreach (
$t as $trace) {
399             if (isset(
$trace['object']) && $trace['object'] instanceof lime_test) {
400                 return array(
$trace['file'], $trace['line']);
401             }
402         }
403
404         
// return the first call
405         
$last count($traces) - 1;
406         return array(
$traces[$last]['file'], $traces[$last]['line']);
407     }
408
409     public function 
handle_error($code$message$file$line$context) {
410         if ( ! 
$this->options['error_reporting'] || ($code error_reporting()) == 0)  {
411             return 
FALSE;
412         }
413
414         switch (
$code) {
415             case 
E_WARNING:
416                 
$type 'Warning';
417                 break;
418             
419             default:
420                 
$type 'Notice';
421                 break;
422         }
423
424         
$trace debug_backtrace();
425         
array_shift($trace); // remove the handle_error() call from the trace
426
427         
$this->error($type.': '.$message$file$line$trace);
428     }
429
430     public function 
handle_exception(Exception $exception) {
431         
$this->error(get_class($exception).': '.$exception->getMessage(), $exception->getFile(), $exception->getLine(), $exception->getTrace());
432
433         
// exception was handled
434         
return TRUE;
435     }
436 }
437
438 class 
lime_output {
439     public 
$colorizer NULL;
440     public 
$base_dir NULL;
441
442     public function 
__construct($force_colors FALSE$base_dir NULL) {
443         
$this->colorizer = new lime_colorizer($force_colors);
444         
$this->base_dir $base_dir === NULL getcwd() : $base_dir;
445     }
446
447     public function 
diag() {
448         
$messages func_get_args();
449         foreach (
$messages as $message) {
450             echo 
$this->colorizer->colorize('# '.join("\n# ", (array) $message), 'COMMENT')."\n";
451         }
452     }
453
454     public function 
comment($message) {
455         echo 
$this->colorizer->colorize(sprintf('# %s'$message), 'COMMENT')."\n";
456     }
457
458     public function 
info($message) {
459         echo 
$this->colorizer->colorize(sprintf('> %s'$message), 'INFO_BAR')."\n";
460     }
461
462     public function 
error($message$file NULL$line NULL$traces = array()) {
463         if (
$file !== NULL) {
464             
$message .= sprintf("\n(in %s on line %s)"$file$line);
465         }
466
467         
// some error messages contain absolute file paths
468         
$message $this->strip_base_dir($message);
469
470         
$space $this->colorizer->colorize(str_repeat(' '71), 'RED_BAR')."\n";
471         
$message trim($message);
472         
$message wordwrap($message66"\n");
473
474         echo 
"\n".$space;
475         foreach (
explode("\n"$message) as $message_line) {
476             echo 
$this->colorizer->colorize(str_pad('  '.$message_line71' '), 'RED_BAR')."\n";
477         }
478         echo 
$space."\n";
479
480         if (
count($traces) > 0) {
481             echo 
$this->colorizer->colorize('Exception trace:''COMMENT')."\n";
482
483             
$this->print_trace(NULL$file$line);
484
485             foreach (
$traces as $trace) {
486                 if (
array_key_exists('class'$trace)) {
487                     
$method sprintf('%s%s%s()'$trace['class'], $trace['type'], $trace['function']);
488                 } else {
489                     
$method sprintf('%s()'$trace['function']);
490                 }
491
492                 if (
array_key_exists('file'$trace)) {
493                     
$this->print_trace($method$trace['file'], $trace['line']);
494                 } else {
495                     
$this->print_trace($method);
496                 }
497             }
498
499             echo 
"\n";
500         }
501     }
502
503     protected function 
print_trace($method NULL$file NULL$line NULL) {
504         if ( ! 
is_null($method)) {
505             
$method .= ' ';
506         }
507
508         echo 
'  '.$method.'at ';
509
510         if ( ! 
is_null($file) && ! is_null($line)) {
511             
printf("%s:%s\n"$this->colorizer->colorize($this->strip_base_dir($file), 'TRACE'), $this->colorizer->colorize($line'TRACE'));
512         } else {
513             echo 
"[internal function]\n";
514         }
515     }
516
517     public function 
echoln($message$colorizer_parameter NULL$colorize TRUE) {
518         if (
$colorize) {
519             
$message preg_replace('/(?:^|\.)((?:not ok|dubious|errors) *\d*)\b/e''$this->colorizer->colorize(\'$1\', \'ERROR\')'$message);
520             
$message preg_replace('/(?:^|\.)(ok *\d*)\b/e''$this->colorizer->colorize(\'$1\', \'INFO\')'$message);
521             
$message preg_replace('/"(.+?)"/e''$this->colorizer->colorize(\'$1\', \'PARAMETER\')'$message);
522             
$message preg_replace('/(\->|\:\:)?([a-zA-Z0-9_]+?)\(\)/e''$this->colorizer->colorize(\'$1$2()\', \'PARAMETER\')'$message);
523         }
524
525         echo (
$colorizer_parameter $this->colorizer->colorize($message$colorizer_parameter) : $message)."\n";
526     }
527
528     public function 
green_bar($message) {
529         echo 
$this->colorizer->colorize($message.str_repeat(' '71 min(71strlen($message))), 'GREEN_BAR')."\n";
530     }
531
532     public function 
red_bar($message) {
533         echo 
$this->colorizer->colorize($message.str_repeat(' '71 min(71strlen($message))), 'RED_BAR')."\n";
534     }
535
536     protected function 
strip_base_dir($text) {
537         return 
str_replace(DIRECTORY_SEPARATOR'/'str_replace(realpath($this->base_dir).DIRECTORY_SEPARATOR''$text));
538     }
539 }
540
541 class 
lime_output_color extends lime_output {}
542
543 class 
lime_colorizer {
544     static public 
$styles = array();
545
546     protected 
$colors_supported FALSE;
547
548     public function 
__construct($force_colors FALSE) {
549         if (
$force_colors) {
550             
$this->colors_supported TRUE;
551         } else {
552             
// colors are supported on windows with ansicon or on tty consoles
553             
if (DIRECTORY_SEPARATOR == '\\') {
554                  
$this->colors_supported FALSE !== getenv('ANSICON');
555             } else {
556                 
$this->colors_supported function_exists('posix_isatty') && @posix_isatty(STDOUT);
557             }
558         }
559     }
560
561     public static function 
style($name$options = array()) {
562         
self::$styles[$name] = $options;
563     }
564
565     public function 
colorize($text ''$parameters = array()) {
566
567         if ( ! 
$this->colors_supported) {
568             return 
$text;
569         }
570
571         static 
$options    = array('bold' => 1'underscore' => 4'blink' => 5'reverse' => 7'conceal' => 8);
572         static 
$foreground = array('black' => 30'red' => 31'green' => 32'yellow' => 33'blue' => 34'magenta' => 35'cyan' => 36'white' => 37);
573         static 
$background = array('black' => 40'red' => 41'green' => 42'yellow' => 43'blue' => 44'magenta' => 45'cyan' => 46'white' => 47);
574
575         !
is_array($parameters) && isset(self::$styles[$parameters]) and $parameters self::$styles[$parameters];
576
577         
$codes = array();
578         isset(
$parameters['fg']) and $codes[] = $foreground[$parameters['fg']];
579         isset(
$parameters['bg']) and $codes[] = $background[$parameters['bg']];
580         foreach (
$options as $option => $value) {
581             isset(
$parameters[$option]) && $parameters[$option] and $codes[] = $value;
582         }
583
584         return 
"\033[".implode(';'$codes).'m'.$text."\033[0m";
585     }
586 }
587
588
lime_colorizer::style('ERROR', array('bg' => 'red''fg' => 'white''bold' => TRUE));
589
lime_colorizer::style('INFO', array('fg' => 'green''bold' => TRUE));
590
lime_colorizer::style('TRACE', array('fg' => 'green''bold' => TRUE));
591
lime_colorizer::style('PARAMETER', array('fg' => 'cyan'));
592
lime_colorizer::style('COMMENT', array('fg' => 'yellow'));
593
594
lime_colorizer::style('GREEN_BAR', array('fg' => 'white''bg' => 'green''bold' => TRUE));
595
lime_colorizer::style('RED_BAR', array('fg' => 'white''bg' => 'red''bold' => TRUE));
596
lime_colorizer::style('INFO_BAR', array('fg' => 'cyan''bold' => TRUE));
597

Page URI: http://www.infopotato.com/index.php/code/core/lime/