lime_test::include_ok()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 7
cp 0
crap 6
1
<?php
2
3
/**
4
 * Unit test library.
5
 *
6
 * @package    lime
7
 * @author     Fabien Potencier <[email protected]>
8
 * @version    SVN: $Id$
9
 */
10
class lime_test
11
{
12
    const EPSILON = 0.0000000001;
13
14
    protected $test_nb = 0;
15
    protected $output = null;
16
    protected $results = array();
17
    protected $options = array();
18
19
    static protected $all_results = array();
20
21
    /**
22
     * Constructor for the lime.php class
23
     *
24
     * @param null $plan
25
     * @param array $options
26
     */
27
    public function __construct($plan = null, $options = array())
28
    {
29
        // for BC
30
        if (!is_array($options)) {
31
            $options = array('output' => $options);
32
        }
33
34
        $this->options = array_merge(array(
35
            'force_colors' => false,
36
            'output' => null,
37
            'verbose' => false,
38
            'error_reporting' => false,
39
        ), $options);
40
41
        $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']);
42
43
        $caller = $this->find_caller(debug_backtrace());
44
        self::$all_results[] = array(
45
            'file' => $caller[0],
46
            'tests' => array(),
47
            'stats' => array(
48
                'plan' => $plan,
49
                'total' => 0,
50
                'failed' => array(),
51
                'passed' => array(),
52
                'skipped' => array(),
53
                'errors' => array()
54
            ),
55
        );
56
57
        $this->results = &self::$all_results[count(self::$all_results) - 1];
58
59
        null !== $plan and $this->output->echoln(sprintf("1..%d", $plan));
60
61
        set_error_handler(array($this, 'handle_error'));
62
        set_exception_handler(array($this, 'handle_exception'));
63
    }
64
65
    static public function reset()
66
    {
67
        self::$all_results = array();
68
    }
69
70
    /**
71
     * @return array
72
     */
73
    static public function to_array()
74
    {
75
        return self::$all_results;
76
    }
77
78
    /**
79
     * @param null $results
80
     * @return string
81
     */
82
    static public function to_xml($results = null)
83
    {
84
        if (is_null($results)) {
85
            $results = self::$all_results;
86
        }
87
88
        $dom = new DOMDocument('1.0', 'UTF-8');
89
        $dom->formatOutput = true;
90
        $dom->appendChild($testsuites = $dom->createElement('testsuites'));
91
92
        $failures = 0;
93
        $errors = 0;
94
        $skipped = 0;
95
        $assertions = 0;
96
97
        foreach ($results as $result) {
98
            $testsuites->appendChild($testsuite = $dom->createElement('testsuite'));
99
            $testsuite->setAttribute('name', basename($result['file'], '.php'));
100
            $testsuite->setAttribute('file', $result['file']);
101
            $testsuite->setAttribute('failures', count($result['stats']['failed']));
102
            $testsuite->setAttribute('errors', count($result['stats']['errors']));
103
            $testsuite->setAttribute('skipped', count($result['stats']['skipped']));
104
            $testsuite->setAttribute('tests', $result['stats']['plan']);
105
            $testsuite->setAttribute('assertions', $result['stats']['plan']);
106
107
            $failures += count($result['stats']['failed']);
108
            $errors += count($result['stats']['errors']);
109
            $skipped += count($result['stats']['skipped']);
110
            $assertions += $result['stats']['plan'];
111
112
            foreach ($result['tests'] as $test) {
113
                $testsuite->appendChild($testcase = $dom->createElement('testcase'));
114
                $testcase->setAttribute('name', $test['message']);
115
                $testcase->setAttribute('file', $test['file']);
116
                $testcase->setAttribute('line', $test['line']);
117
                $testcase->setAttribute('assertions', 1);
118
                if (!$test['status']) {
119
                    $testcase->appendChild($failure = $dom->createElement('failure'));
120
                    $failure->setAttribute('type', 'lime');
121
                    if (isset($test['error'])) {
122
                        $failure->appendChild($dom->createTextNode($test['error']));
123
                    }
124
                }
125
            }
126
        }
127
128
        $testsuites->setAttribute('failures', $failures);
129
        $testsuites->setAttribute('errors', $errors);
130
        $testsuites->setAttribute('tests', $assertions);
131
        $testsuites->setAttribute('assertions', $assertions);
132
        $testsuites->setAttribute('skipped', $skipped);
133
134
        return $dom->saveXml();
135
    }
136
137
    /**
138
     *
139
     */
140
    public function __destruct()
141
    {
142
        $plan = $this->results['stats']['plan'];
143
        $passed = count($this->results['stats']['passed']);
144
        $failed = count($this->results['stats']['failed']);
145
        $total = $this->results['stats']['total'];
146
        is_null($plan) and $plan = $total and $this->output->echoln(sprintf("1..%d", $plan));
147
148
        if ($total > $plan) {
149
            $this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra.", $plan,
150
                $total - $plan));
151
        } elseif ($total < $plan) {
152
            $this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d.", $plan, $total));
153
        }
154
155
        if ($failed) {
156
            $this->output->red_bar(sprintf("# Looks like you failed %d tests of %d.", $failed, $passed + $failed));
157
        } else {
158
            if ($total == $plan) {
159
                $this->output->green_bar("# Looks like everything went fine.");
160
            }
161
        }
162
163
        flush();
164
    }
165
166
    /**
167
     * Tests a condition and passes if it is true
168
     *
169
     * @param mixed $exp condition to test
170
     * @param string $message display output message when the test passes
171
     *
172
     * @return boolean
173
     */
174
    public function ok($exp, $message = '')
175
    {
176
        $this->update_stats();
177
178
        if ($result = (boolean)$exp) {
179
            $this->results['stats']['passed'][] = $this->test_nb;
180
        } else {
181
            $this->results['stats']['failed'][] = $this->test_nb;
182
        }
183
        $this->results['tests'][$this->test_nb]['message'] = $message;
184
        $this->results['tests'][$this->test_nb]['status'] = $result;
185
        $this->output->echoln(sprintf("%s %d%s", $result ? 'ok' : 'not ok', $this->test_nb,
186
            $message = $message ? sprintf('%s %s', 0 === strpos($message, '#') ? '' : ' -', $message) : ''));
187
188
        if (!$result) {
189
            $this->output->diag(sprintf('    Failed test (%s at line %d)',
190
                str_replace(getcwd(), '.', $this->results['tests'][$this->test_nb]['file']),
191
                $this->results['tests'][$this->test_nb]['line']));
192
        }
193
194
        return $result;
195
    }
196
197
    /**
198
     * Compares two values and passes if they are equal (==)
199
     *
200
     * @param mixed $exp1 left value
201
     * @param mixed $exp2 right value
202
     * @param string $message display output message when the test passes
203
     *
204
     * @return boolean
205
     */
206
    public function is($exp1, $exp2, $message = '')
207
    {
208
        if (is_object($exp1) || is_object($exp2)) {
209
            $value = $exp1 === $exp2;
210
        } else {
211
            if (is_float($exp1) && is_float($exp2)) {
212
                $value = abs($exp1 - $exp2) < self::EPSILON;
213
            } else {
214
                $value = $exp1 == $exp2;
215
            }
216
        }
217
218
        if (!$result = $this->ok($value, $message)) {
219
            $this->set_last_test_errors(array(
220
                sprintf("           got: %s", var_export($exp1, true)),
221
                sprintf("      expected: %s", var_export($exp2, true))
222
            ));
223
        }
224
225
        return $result;
226
    }
227
228
    /**
229
     * Compares two values and passes if they are not equal
230
     *
231
     * @param mixed $exp1 left value
232
     * @param mixed $exp2 right value
233
     * @param string $message display output message when the test passes
234
     *
235
     * @return boolean
236
     */
237
    public function isnt($exp1, $exp2, $message = '')
238
    {
239
        if (!$result = $this->ok($exp1 != $exp2, $message)) {
240
            $this->set_last_test_errors(array(
241
                sprintf("      %s", var_export($exp2, true)),
242
                '          ne',
243
                sprintf("      %s", var_export($exp2, true))
244
            ));
245
        }
246
247
        return $result;
248
    }
249
250
    /**
251
     * Tests a string against a regular expression
252
     *
253
     * @param string $exp value to test
254
     * @param string $regex the pattern to search for, as a string
255
     * @param string $message display output message when the test passes
256
     *
257
     * @return boolean
258
     */
259
    public function like($exp, $regex, $message = '')
260
    {
261
        if (!$result = $this->ok(preg_match($regex, $exp), $message)) {
262
            $this->set_last_test_errors(array(
263
                sprintf("                    '%s'", $exp),
264
                sprintf("      doesn't match '%s'", $regex)
265
            ));
266
        }
267
268
        return $result;
269
    }
270
271
    /**
272
     * Checks that a string doesn't match a regular expression
273
     *
274
     * @param string $exp value to test
275
     * @param string $regex the pattern to search for, as a string
276
     * @param string $message display output message when the test passes
277
     *
278
     * @return boolean
279
     */
280
    public function unlike($exp, $regex, $message = '')
281
    {
282
        if (!$result = $this->ok(!preg_match($regex, $exp), $message)) {
283
            $this->set_last_test_errors(array(
284
                sprintf("               '%s'", $exp),
285
                sprintf("      matches '%s'", $regex)
286
            ));
287
        }
288
289
        return $result;
290
    }
291
292
    /**
293
     * Compares two arguments with an operator
294
     *
295
     * @param mixed $exp1 left value
296
     * @param string $op operator
297
     * @param mixed $exp2 right value
298
     * @param string $message display output message when the test passes
299
     *
300
     * @return boolean
301
     */
302
    public function cmp_ok($exp1, $op, $exp2, $message = '')
303
    {
304
        $result = false;
305
        $php = sprintf("\$result = \$exp1 $op \$exp2;");
306
        // under some unknown conditions the sprintf() call causes a segmentation fault
307
        // when placed directly in the eval() call
308
        eval($php);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
309
310
        if (!$this->ok($result, $message)) {
311
            $this->set_last_test_errors(array(
312
                sprintf("      %s", str_replace("\n", '', var_export($exp1, true))),
313
                sprintf("          %s", $op),
314
                sprintf("      %s", str_replace("\n", '', var_export($exp2, true)))
315
            ));
316
        }
317
318
        return $result;
319
    }
320
321
    /**
322
     * Checks the availability of a method for an object or a class
323
     *
324
     * @param mixed $object an object instance or a class name
325
     * @param string|array $methods one or more method names
326
     * @param string $message display output message when the test passes
327
     *
328
     * @return boolean
329
     */
330
    public function can_ok($object, $methods, $message = '')
331
    {
332
        $result = true;
333
        $failed_messages = array();
334
        foreach ((array)$methods as $method) {
335
            if (!method_exists($object, $method)) {
336
                $failed_messages[] = sprintf("      method '%s' does not exist", $method);
337
                $result = false;
338
            }
339
        }
340
341
        !$this->ok($result, $message);
342
343
        !$result and $this->set_last_test_errors($failed_messages);
344
345
        return $result;
346
    }
347
348
    /**
349
     * Checks the type of an argument
350
     *
351
     * @param mixed $var variable instance
352
     * @param string $class class or type name
353
     * @param string $message display output message when the test passes
354
     *
355
     * @return boolean
356
     */
357
    public function isa_ok($var, $class, $message = '')
358
    {
359
        $type = is_object($var) ? get_class($var) : gettype($var);
360
        if (!$result = $this->ok($type == $class, $message)) {
361
            $this->set_last_test_errors(array(sprintf("      variable isn't a '%s' it's a '%s'", $class, $type)));
362
        }
363
364
        return $result;
365
    }
366
367
    /**
368
     * Checks that two arrays have the same values
369
     *
370
     * @param mixed $exp1 first variable
371
     * @param mixed $exp2 second variable
372
     * @param string $message display output message when the test passes
373
     *
374
     * @return boolean
375
     */
376
    public function is_deeply($exp1, $exp2, $message = '')
377
    {
378
        if (!$result = $this->ok($this->test_is_deeply($exp1, $exp2), $message)) {
379
            $this->set_last_test_errors(array(
380
                sprintf("           got: %s", str_replace("\n", '', var_export($exp1, true))),
381
                sprintf("      expected: %s", str_replace("\n", '', var_export($exp2, true)))
382
            ));
383
        }
384
385
        return $result;
386
    }
387
388
    /**
389
     * is_ignore_nl function.
390
     *
391
     * @access public
392
     * @param mixed $exp1
393
     * @param mixed $exp2
394
     * @param string $message (default: '')
395
     * @return boolean
396
     */
397
    public function is_ignore_nl($exp1, $exp2, $message = '')
398
    {
399
        return $this->is(
400
            str_replace("\n", '', $exp1),
401
            str_replace("\n", '', $exp2),
402
            $message
403
        );
404
    }
405
406
    /**
407
     * Sortcut for {@link is}(), performs a strict check.
408
     *
409
     * @access public
410
     * @param mixed $exp1
411
     * @param mixed $exp2
412
     * @param string $message (default: '')
413
     * @return boolean
414
     */
415
    public function is_strict($exp1, $exp2, $message = '')
416
    {
417
        if (is_float($exp1) && is_float($exp2)) {
418
            $value = abs($exp1 - $exp2) < self::EPSILON;
419
        } else {
420
            $value = $exp1 === $exp2;
421
        }
422
423
        if (!$result = $this->ok($value, $message)) {
424
            $this->set_last_test_errors(array(
425
                sprintf("           got: (%s) %s", gettype($exp1), var_export($exp1, true)),
426
                sprintf("      expected: (%s) %s", gettype($exp2), var_export($exp2, true))
427
            ));
428
        }
429
430
        return $result;
431
    }
432
433
    /**
434
     * Sortcut for {@link isnt}(), performs a strict check.
435
     *
436
     * @access public
437
     * @param mixed $exp1
438
     * @param mixed $exp2
439
     * @param string $message . (default: '')
440
     * @return boolean
441
     */
442
    public function isnt_strict($exp1, $exp2, $message = '')
443
    {
444
        if (!$result = $this->ok($exp1 !== $exp2, $message)) {
445
            $this->set_last_test_errors(array(
446
                sprintf("      (%s) %s", gettype($exp1), var_export($exp1, true)),
447
                '          ne',
448
                sprintf("      (%s) %s", gettype($exp2), var_export($exp2, true))
449
            ));
450
        }
451
452
        return $result;
453
    }
454
455
    /**
456
     * Always passes--useful for testing exceptions
457
     *
458
     * @param string $message display output message
459
     *
460
     * @return boolean
461
     */
462
    public function pass($message = '')
463
    {
464
        return $this->ok(true, $message);
465
    }
466
467
    /**
468
     * Always fails--useful for testing exceptions
469
     *
470
     * @param string $message display output message
471
     *
472
     * @return false
473
     */
474
    public function fail($message = '')
475
    {
476
        return $this->ok(false, $message);
477
    }
478
479
    /**
480
     * Outputs a diag message but runs no test
481
     *
482
     * @param string $message display output message
483
     *
484
     * @return void
485
     */
486
    public function diag($message)
487
    {
488
        $this->output->diag($message);
489
    }
490
491
    /**
492
     * Counts as $nb_tests tests--useful for conditional tests
493
     *
494
     * @param string $message display output message
495
     * @param integer $nb_tests number of tests to skip
496
     *
497
     * @return void
498
     */
499
    public function skip($message = '', $nb_tests = 1)
500
    {
501
        for ($i = 0; $i < $nb_tests; $i++) {
502
            $this->pass(sprintf("# SKIP%s", $message ? ' ' . $message : ''));
503
            $this->results['stats']['skipped'][] = $this->test_nb;
504
            array_pop($this->results['stats']['passed']);
505
        }
506
    }
507
508
    /**
509
     * Counts as a test--useful for tests yet to be written
510
     *
511
     * @param string $message display output message
512
     *
513
     * @return void
514
     */
515
    public function todo($message = '')
516
    {
517
        $this->pass(sprintf("# TODO%s", $message ? ' ' . $message : ''));
518
        $this->results['stats']['skipped'][] = $this->test_nb;
519
        array_pop($this->results['stats']['passed']);
520
    }
521
522
    /**
523
     * Validates that a file exists and that it is properly included
524
     *
525
     * @param string $file file path
526
     * @param string $message display output message when the test passes
527
     *
528
     * @return boolean
529
     */
530
    public function include_ok($file, $message = '')
531
    {
532
        if (!$result = $this->ok((@include($file)) == 1, $message)) {
533
            $this->set_last_test_errors(array(sprintf("      Tried to include '%s'", $file)));
534
        }
535
536
        return $result;
537
    }
538
539
    /**
540
     * @param $var1
541
     * @param $var2
542
     * @return bool
543
     */
544
    private function test_is_deeply($var1, $var2)
545
    {
546
        if (gettype($var1) != gettype($var2)) {
547
            return false;
548
        }
549
550
        if (is_array($var1)) {
551
            ksort($var1);
552
            ksort($var2);
553
554
            $keys1 = array_keys($var1);
555
            $keys2 = array_keys($var2);
556
            if (array_diff($keys1, $keys2) || array_diff($keys2, $keys1)) {
557
                return false;
558
            }
559
            $is_equal = true;
560
            foreach ($var1 as $key => $value) {
561
                $is_equal = $this->test_is_deeply($var1[$key], $var2[$key]);
562
                if ($is_equal === false) {
563
                    break;
564
                }
565
            }
566
567
            return $is_equal;
568
        } else {
569
            return $var1 === $var2;
570
        }
571
    }
572
573
    /**
574
     * @param $message
575
     */
576
    public function comment($message)
577
    {
578
        $this->output->comment($message);
579
    }
580
581
    /**
582
     * @param $message
583
     */
584
    public function info($message)
585
    {
586
        $this->output->info($message);
587
    }
588
589
    /**
590
     * @param string $message
591
     */
592
    public function error($message, $file = null, $line = null, array $traces = array())
593
    {
594
        $this->output->error($message, $file, $line, $traces);
595
596
        $this->results['stats']['errors'][] = array(
597
            'message' => $message,
598
            'file' => $file,
599
            'line' => $line,
600
        );
601
    }
602
603
    protected function update_stats()
604
    {
605
        ++$this->test_nb;
606
        ++$this->results['stats']['total'];
607
608
        list($this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace());
609
    }
610
611
    /**
612
     * @param array $errors
613
     */
614
    protected function set_last_test_errors(array $errors)
615
    {
616
        $this->output->diag($errors);
617
618
        $this->results['tests'][$this->test_nb]['error'] = implode("\n", $errors);
619
    }
620
621
    /**
622
     * @param $traces
623
     * @return array
624
     */
625
    protected function find_caller($traces)
626
    {
627
        // find the first call to a method of an object that is an instance of lime_test
628
        $t = array_reverse($traces);
629
        foreach ($t as $trace) {
630
            if (isset($trace['object']) && $trace['object'] instanceof lime_test) {
631
                return array($trace['file'], $trace['line']);
632
            }
633
        }
634
635
        // return the first call
636
        $last = count($traces) - 1;
637
638
        return array($traces[$last]['file'], $traces[$last]['line']);
639
    }
640
641
    /**
642
     * @param $code
643
     * @param $message
644
     * @param $file
645
     * @param $line
646
     * @param $context
647
     * @return bool
648
     *
649
     * @FIXME:  This method has inconsistent return types
650
     */
651
    public function handle_error($code, $message, $file, $line, $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
652
    {
653
        if (!$this->options['error_reporting'] || ($code & error_reporting()) == 0) {
654
            return false;
655
        }
656
657
        switch ($code) {
658
            case E_WARNING:
659
                $type = 'Warning';
660
                break;
661
            default:
662
                $type = 'Notice';
663
                break;
664
        }
665
666
        $trace = debug_backtrace();
667
        array_shift($trace); // remove the handle_error() call from the trace
668
669
        $this->error($type . ': ' . $message, $file, $line, $trace);
670
    }
671
672
    /**
673
     * @param Exception $exception
674
     * @return bool
675
     */
676
    public function handle_exception(Exception $exception)
677
    {
678
        $this->error(get_class($exception) . ': ' . $exception->getMessage(), $exception->getFile(),
679
            $exception->getLine(), $exception->getTrace());
680
681
        // exception was handled
682
        return true;
683
    }
684
}