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 © 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', 0 === 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($exp1, TRUE)), sprintf(" expected: %s", var_export($exp2, TRUE))));
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($exp2, TRUE)), ' ne', sprintf(" %s", var_export($exp2, TRUE))));
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($exp1, TRUE))), sprintf(" %s", $op), sprintf(" %s", str_replace("\n", '', var_export($exp2, TRUE)))));
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($exp1, TRUE))), sprintf(" expected: %s", str_replace("\n", '', var_export($exp2, TRUE)))));
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($message, 66, "\n");
473
474
echo "\n".$space;
475
foreach (explode("\n", $message) as $message_line) {
476
echo $this->colorizer->colorize(str_pad(' '.$message_line, 71, ' '), '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(71, strlen($message))), 'GREEN_BAR')."\n";
530
}
531
532
public function red_bar($message) {
533
echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($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/
