Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/**
4
 * This file is part of the symfony package.
5
 * (c) 2004-2006 Fabien Potencier <fabien.potencier@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
 
11
/**
12
 * Unit test library.
13
 *
14
 * @package    lime
15
 * @author     Fabien Potencier <fabien.potencier@gmail.com>
16
 * @version    SVN: $Id: lime.php 29529 2010-05-19 13:41:48Z fabien $
17
 */
18
class lime_test
19
{
20
  const EPSILON = 0.0000000001;
21
 
22
  protected $test_nb = 0;
23
  protected $output  = null;
24
  protected $results = array();
25
  protected $options = array();
26
 
27
  static protected $all_results = array();
28
 
29
  public function __construct($plan = null, $options = array())
30
  {
31
    // for BC
32
    if (!is_array($options))
33
    {
34
      $options = array('output' => $options);
35
    }
36
 
37
    $this->options = array_merge(array(
38
      'force_colors'    => false,
39
      'output'          => null,
40
      'verbose'         => false,
41
      'error_reporting' => false,
42
    ), $options);
43
 
44
    $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']);
45
 
46
    $caller = $this->find_caller(debug_backtrace());
47
    self::$all_results[] = array(
48
      'file'  => $caller[0],
49
      'tests' => array(),
50
      'stats' => array('plan' => $plan, 'total' => 0, 'failed' => array(), 'passed' => array(), 'skipped' => array(), 'errors' => array()),
51
    );
52
 
53
    $this->results = &self::$all_results[count(self::$all_results) - 1];
54
 
55
    null !== $plan and $this->output->echoln(sprintf("1..%d", $plan));
56
 
57
    set_error_handler(array($this, 'handle_error'));
58
    set_exception_handler(array($this, 'handle_exception'));
59
  }
60
 
61
  static public function reset()
62
  {
63
    self::$all_results = array();
64
  }
65
 
66
  static public function to_array()
67
  {
68
    return self::$all_results;
69
  }
70
 
71
  static public function to_xml($results = null)
72
  {
73
    if (is_null($results))
74
    {
75
      $results = self::$all_results;
76
    }
77
 
78
    $dom = new DOMDocument('1.0', 'UTF-8');
79
    $dom->formatOutput = true;
80
    $dom->appendChild($testsuites = $dom->createElement('testsuites'));
81
 
82
    $errors = 0;
83
    $failures = 0;
84
    $errors = 0;
85
    $skipped = 0;
86
    $assertions = 0;
87
 
88
    foreach ($results as $result)
89
    {
90
      $testsuites->appendChild($testsuite = $dom->createElement('testsuite'));
91
      $testsuite->setAttribute('name', basename($result['file'], '.php'));
92
      $testsuite->setAttribute('file', $result['file']);
93
      $testsuite->setAttribute('failures', count($result['stats']['failed']));
94
      $testsuite->setAttribute('errors', count($result['stats']['errors']));
95
      $testsuite->setAttribute('skipped', count($result['stats']['skipped']));
96
      $testsuite->setAttribute('tests', $result['stats']['plan']);
97
      $testsuite->setAttribute('assertions', $result['stats']['plan']);
98
 
99
      $failures += count($result['stats']['failed']);
100
      $errors += count($result['stats']['errors']);
101
      $skipped += count($result['stats']['skipped']);
102
      $assertions += $result['stats']['plan'];
103
 
104
      foreach ($result['tests'] as $test)
105
      {
106
        $testsuite->appendChild($testcase = $dom->createElement('testcase'));
107
        $testcase->setAttribute('name', $test['message']);
108
        $testcase->setAttribute('file', $test['file']);
109
        $testcase->setAttribute('line', $test['line']);
110
        $testcase->setAttribute('assertions', 1);
111
        if (!$test['status'])
112
        {
113
          $testcase->appendChild($failure = $dom->createElement('failure'));
114
          $failure->setAttribute('type', 'lime');
115
          if (isset($test['error']))
116
          {
117
            $failure->appendChild($dom->createTextNode($test['error']));
118
          }
119
        }
120
      }
121
    }
122
 
123
    $testsuites->setAttribute('failures', $failures);
124
    $testsuites->setAttribute('errors', $errors);
125
    $testsuites->setAttribute('tests', $assertions);
126
    $testsuites->setAttribute('assertions', $assertions);
127
    $testsuites->setAttribute('skipped', $skipped);
128
 
129
    return $dom->saveXml();
130
  }
131
 
132
  public function __destruct()
133
  {
134
    $plan = $this->results['stats']['plan'];
135
    $passed = count($this->results['stats']['passed']);
136
    $failed = count($this->results['stats']['failed']);
137
    $total = $this->results['stats']['total'];
138
    is_null($plan) and $plan = $total and $this->output->echoln(sprintf("1..%d", $plan));
139
 
140
    if ($total > $plan)
141
    {
142
      $this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra.", $plan, $total - $plan));
143
    }
144
    elseif ($total < $plan)
145
    {
146
      $this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d.", $plan, $total));
147
    }
148
 
149
    if ($failed)
150
    {
151
      $this->output->red_bar(sprintf("# Looks like you failed %d tests of %d.", $failed, $passed + $failed));
152
    }
153
    else if ($total == $plan)
154
    {
155
      $this->output->green_bar("# Looks like everything went fine.");
156
    }
157
 
158
    flush();
159
  }
160
 
161
  /**
162
   * Tests a condition and passes if it is true
163
   *
164
   * @param mixed  $exp     condition to test
165
   * @param string $message display output message when the test passes
166
   *
167
   * @return boolean
168
   */
169
  public function ok($exp, $message = '')
170
  {
171
    $this->update_stats();
172
 
173
    if ($result = (boolean) $exp)
174
    {
175
      $this->results['stats']['passed'][] = $this->test_nb;
176
    }
177
    else
178
    {
179
      $this->results['stats']['failed'][] = $this->test_nb;
180
    }
181
    $this->results['tests'][$this->test_nb]['message'] = $message;
182
    $this->results['tests'][$this->test_nb]['status'] = $result;
183
    $this->output->echoln(sprintf("%s %d%s", $result ? 'ok' : 'not ok', $this->test_nb, $message = $message ? sprintf('%s %s', 0 === strpos($message, '#') ? '' : ' -', $message) : ''));
184
 
185
    if (!$result)
186
    {
187
      $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']));
188
    }
189
 
190
    return $result;
191
  }
192
 
193
  /**
194
   * Compares two values and passes if they are equal (==)
195
   *
196
   * @param mixed  $exp1    left value
197
   * @param mixed  $exp2    right value
198
   * @param string $message display output message when the test passes
199
   *
200
   * @return boolean
201
   */
202
  public function is($exp1, $exp2, $message = '')
203
  {
204
    if (is_object($exp1) || is_object($exp2))
205
    {
206
      $value = $exp1 === $exp2;
207
    }
208
    else if (is_float($exp1) && is_float($exp2))
209
    {
210
      $value = abs($exp1 - $exp2) < self::EPSILON;
211
    }
212
    else
213
    {
214
      $value = $exp1 == $exp2;
215
    }
216
 
217
    if (!$result = $this->ok($value, $message))
218
    {
219
      $this->set_last_test_errors(array(sprintf("           got: %s", var_export($exp1, true)), sprintf("      expected: %s", var_export($exp2, true))));
220
    }
221
 
222
    return $result;
223
  }
224
 
225
  /**
226
   * Compares two values and passes if they are not equal
227
   *
228
   * @param mixed  $exp1    left value
229
   * @param mixed  $exp2    right value
230
   * @param string $message display output message when the test passes
231
   *
232
   * @return boolean
233
   */
234
  public function isnt($exp1, $exp2, $message = '')
235
  {
236
    if (!$result = $this->ok($exp1 != $exp2, $message))
237
    {
238
      $this->set_last_test_errors(array(sprintf("      %s", var_export($exp2, true)), '          ne', sprintf("      %s", var_export($exp2, true))));
239
    }
240
 
241
    return $result;
242
  }
243
 
244
  /**
245
   * Tests a string against a regular expression
246
   *
247
   * @param string $exp     value to test
248
   * @param string $regex   the pattern to search for, as a string
249
   * @param string $message display output message when the test passes
250
   *
251
   * @return boolean
252
   */
253
  public function like($exp, $regex, $message = '')
254
  {
255
    if (!$result = $this->ok(preg_match($regex, $exp), $message))
256
    {
257
      $this->set_last_test_errors(array(sprintf("                    '%s'", $exp), sprintf("      doesn't match '%s'", $regex)));
258
    }
259
 
260
    return $result;
261
  }
262
 
263
  /**
264
   * Checks that a string doesn't match a regular expression
265
   *
266
   * @param string $exp     value to test
267
   * @param string $regex   the pattern to search for, as a string
268
   * @param string $message display output message when the test passes
269
   *
270
   * @return boolean
271
   */
272
  public function unlike($exp, $regex, $message = '')
273
  {
274
    if (!$result = $this->ok(!preg_match($regex, $exp), $message))
275
    {
276
      $this->set_last_test_errors(array(sprintf("               '%s'", $exp), sprintf("      matches '%s'", $regex)));
277
    }
278
 
279
    return $result;
280
  }
281
 
282
  /**
283
   * Compares two arguments with an operator
284
   *
285
   * @param mixed  $exp1    left value
286
   * @param string $op      operator
287
   * @param mixed  $exp2    right value
288
   * @param string $message display output message when the test passes
289
   *
290
   * @return boolean
291
   */
292
  public function cmp_ok($exp1, $op, $exp2, $message = '')
293
  {
294
    $php = sprintf("\$result = \$exp1 $op \$exp2;");
295
    // under some unknown conditions the sprintf() call causes a segmentation fault
296
    // when placed directly in the eval() call
297
    eval($php);
298
 
299
    if (!$this->ok($result, $message))
300
    {
301
      $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)))));
302
    }
303
 
304
    return $result;
305
  }
306
 
307
  /**
308
   * Checks the availability of a method for an object or a class
309
   *
310
   * @param mixed        $object  an object instance or a class name
311
   * @param string|array $methods one or more method names
312
   * @param string       $message display output message when the test passes
313
   *
314
   * @return boolean
315
   */
316
  public function can_ok($object, $methods, $message = '')
317
  {
318
    $result = true;
319
    $failed_messages = array();
320
    foreach ((array) $methods as $method)
321
    {
322
      if (!method_exists($object, $method))
323
      {
324
        $failed_messages[] = sprintf("      method '%s' does not exist", $method);
325
        $result = false;
326
      }
327
    }
328
 
329
    !$this->ok($result, $message);
330
 
331
    !$result and $this->set_last_test_errors($failed_messages);
332
 
333
    return $result;
334
  }
335
 
336
  /**
337
   * Checks the type of an argument
338
   *
339
   * @param mixed  $var     variable instance
340
   * @param string $class   class or type name
341
   * @param string $message display output message when the test passes
342
   *
343
   * @return boolean
344
   */
345
  public function isa_ok($var, $class, $message = '')
346
  {
347
    $type = is_object($var) ? get_class($var) : gettype($var);
348
    if (!$result = $this->ok($type == $class, $message))
349
    {
350
      $this->set_last_test_errors(array(sprintf("      variable isn't a '%s' it's a '%s'", $class, $type)));
351
    }
352
 
353
    return $result;
354
  }
355
 
356
  /**
357
   * Checks that two arrays have the same values
358
   *
359
   * @param mixed  $exp1    first variable
360
   * @param mixed  $exp2    second variable
361
   * @param string $message display output message when the test passes
362
   *
363
   * @return boolean
364
   */
365
  public function is_deeply($exp1, $exp2, $message = '')
366
  {
367
    if (!$result = $this->ok($this->test_is_deeply($exp1, $exp2), $message))
368
    {
369
      $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)))));
370
    }
371
 
372
    return $result;
373
  }
374
 
375
  /**
376
   * Always passes--useful for testing exceptions
377
   *
378
   * @param string $message display output message
379
   *
380
   * @return true
381
   */
382
  public function pass($message = '')
383
  {
384
    return $this->ok(true, $message);
385
  }
386
 
387
  /**
388
   * Always fails--useful for testing exceptions
389
   *
390
   * @param string $message display output message
391
   *
392
   * @return false
393
   */
394
  public function fail($message = '')
395
  {
396
    return $this->ok(false, $message);
397
  }
398
 
399
  /**
400
   * Outputs a diag message but runs no test
401
   *
402
   * @param string $message display output message
403
   *
404
   * @return void
405
   */
406
  public function diag($message)
407
  {
408
    $this->output->diag($message);
409
  }
410
 
411
  /**
412
   * Counts as $nb_tests tests--useful for conditional tests
413
   *
414
   * @param string  $message  display output message
415
   * @param integer $nb_tests number of tests to skip
416
   *
417
   * @return void
418
   */
419
  public function skip($message = '', $nb_tests = 1)
420
  {
421
    for ($i = 0; $i < $nb_tests; $i++)
422
    {
423
      $this->pass(sprintf("# SKIP%s", $message ? ' '.$message : ''));
424
      $this->results['stats']['skipped'][] = $this->test_nb;
425
      array_pop($this->results['stats']['passed']);
426
    }
427
  }
428
 
429
  /**
430
   * Counts as a test--useful for tests yet to be written
431
   *
432
   * @param string $message display output message
433
   *
434
   * @return void
435
   */
436
  public function todo($message = '')
437
  {
438
    $this->pass(sprintf("# TODO%s", $message ? ' '.$message : ''));
439
    $this->results['stats']['skipped'][] = $this->test_nb;
440
    array_pop($this->results['stats']['passed']);
441
  }
442
 
443
  /**
444
   * Validates that a file exists and that it is properly included
445
   *
446
   * @param string $file    file path
447
   * @param string $message display output message when the test passes
448
   *
449
   * @return boolean
450
   */
451
  public function include_ok($file, $message = '')
452
  {
453
    if (!$result = $this->ok((@include($file)) == 1, $message))
454
    {
455
      $this->set_last_test_errors(array(sprintf("      Tried to include '%s'", $file)));
456
    }
457
 
458
    return $result;
459
  }
460
 
461
  private function test_is_deeply($var1, $var2)
462
  {
463
    if (gettype($var1) != gettype($var2))
464
    {
465
      return false;
466
    }
467
 
468
    if (is_array($var1))
469
    {
470
      ksort($var1);
471
      ksort($var2);
472
 
473
      $keys1 = array_keys($var1);
474
      $keys2 = array_keys($var2);
475
      if (array_diff($keys1, $keys2) || array_diff($keys2, $keys1))
476
      {
477
        return false;
478
      }
479
      $is_equal = true;
480
      foreach ($var1 as $key => $value)
481
      {
482
        $is_equal = $this->test_is_deeply($var1[$key], $var2[$key]);
483
        if ($is_equal === false)
484
        {
485
          break;
486
        }
487
      }
488
 
489
      return $is_equal;
490
    }
491
    else
492
    {
493
      return $var1 === $var2;
494
    }
495
  }
496
 
497
  public function comment($message)
498
  {
499
    $this->output->comment($message);
500
  }
501
 
502
  public function info($message)
503
  {
504
    $this->output->info($message);
505
  }
506
 
507
  public function error($message, $file = null, $line = null, array $traces = array())
508
  {
509
    $this->output->error($message, $file, $line, $traces);
510
 
511
  	$this->results['stats']['errors'][] = array(
512
  	  'message' => $message,
513
  	  'file' => $file,
514
  	  'line' => $line,
515
  	);
516
  }
517
 
518
  protected function update_stats()
519
  {
520
    ++$this->test_nb;
521
    ++$this->results['stats']['total'];
522
 
523
    list($this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace());
524
  }
525
 
526
  protected function set_last_test_errors(array $errors)
527
  {
528
    $this->output->diag($errors);
529
 
530
    $this->results['tests'][$this->test_nb]['error'] = implode("\n", $errors);
531
  }
532
 
533
  protected function find_caller($traces)
534
  {
535
    // find the first call to a method of an object that is an instance of lime_test
536
    $t = array_reverse($traces);
537
    foreach ($t as $trace)
538
    {
539
      if (isset($trace['object']) && $trace['object'] instanceof lime_test)
540
      {
541
        return array($trace['file'], $trace['line']);
542
      }
543
    }
544
 
545
    // return the first call
546
    $last = count($traces) - 1;
547
    return array($traces[$last]['file'], $traces[$last]['line']);
548
  }
549
 
550
  public function handle_error($code, $message, $file, $line, $context)
551
  {
552
    if (!$this->options['error_reporting'] || ($code & error_reporting()) == 0)
553
    {
554
      return false;
555
    }
556
 
557
    switch ($code)
558
    {
559
      case E_WARNING:
560
        $type = 'Warning';
561
        break;
562
      default:
563
        $type = 'Notice';
564
        break;
565
    }
566
 
567
    $trace = debug_backtrace();
568
    array_shift($trace); // remove the handle_error() call from the trace
569
 
570
    $this->error($type.': '.$message, $file, $line, $trace);
571
  }
572
 
573
  public function handle_exception(Exception $exception)
574
  {
575
    $this->error(get_class($exception).': '.$exception->getMessage(), $exception->getFile(), $exception->getLine(), $exception->getTrace());
576
 
577
    // exception was handled
578
    return true;
579
  }
580
}
581
 
582
class lime_output
583
{
584
  public $colorizer = null;
585
  public $base_dir = null;
586
 
587
  public function __construct($force_colors = false, $base_dir = null)
588
  {
589
    $this->colorizer = new lime_colorizer($force_colors);
590
    $this->base_dir = $base_dir === null ? getcwd() : $base_dir;
591
  }
592
 
593
  public function diag()
594
  {
595
    $messages = func_get_args();
596
    foreach ($messages as $message)
597
    {
598
      echo $this->colorizer->colorize('# '.join("\n# ", (array) $message), 'COMMENT')."\n";
599
    }
600
  }
601
 
602
  public function comment($message)
603
  {
604
    echo $this->colorizer->colorize(sprintf('# %s', $message), 'COMMENT')."\n";
605
  }
606
 
607
  public function info($message)
608
  {
609
    echo $this->colorizer->colorize(sprintf('> %s', $message), 'INFO_BAR')."\n";
610
  }
611
 
612
  public function error($message, $file = null, $line = null, $traces = array())
613
  {
614
    if ($file !== null)
615
    {
616
      $message .= sprintf("\n(in %s on line %s)", $file, $line);
617
    }
618
 
619
    // some error messages contain absolute file paths
620
    $message = $this->strip_base_dir($message);
621
 
622
    $space = $this->colorizer->colorize(str_repeat(' ', 71), 'RED_BAR')."\n";
623
    $message = trim($message);
624
    $message = wordwrap($message, 66, "\n");
625
 
626
    echo "\n".$space;
627
    foreach (explode("\n", $message) as $message_line)
628
    {
629
      echo $this->colorizer->colorize(str_pad('  '.$message_line, 71, ' '), 'RED_BAR')."\n";
630
    }
631
    echo $space."\n";
632
 
633
    if (count($traces) > 0)
634
    {
635
      echo $this->colorizer->colorize('Exception trace:', 'COMMENT')."\n";
636
 
637
      $this->print_trace(null, $file, $line);
638
 
639
      foreach ($traces as $trace)
640
      {
641
        if (array_key_exists('class', $trace))
642
        {
643
          $method = sprintf('%s%s%s()', $trace['class'], $trace['type'], $trace['function']);
644
        }
645
        else
646
        {
647
          $method = sprintf('%s()', $trace['function']);
648
        }
649
 
650
        if (array_key_exists('file', $trace))
651
        {
652
          $this->print_trace($method, $trace['file'], $trace['line']);
653
        }
654
        else
655
        {
656
          $this->print_trace($method);
657
        }
658
      }
659
 
660
      echo "\n";
661
    }
662
  }
663
 
664
  protected function print_trace($method = null, $file = null, $line = null)
665
  {
666
    if (!is_null($method))
667
    {
668
      $method .= ' ';
669
    }
670
 
671
    echo '  '.$method.'at ';
672
 
673
    if (!is_null($file) && !is_null($line))
674
    {
675
      printf("%s:%s\n", $this->colorizer->colorize($this->strip_base_dir($file), 'TRACE'), $this->colorizer->colorize($line, 'TRACE'));
676
    }
677
    else
678
    {
679
      echo "[internal function]\n";
680
    }
681
  }
682
 
683
  public function echoln($message, $colorizer_parameter = null, $colorize = true)
684
  {
685
    if ($colorize)
686
    {
687
      $message = preg_replace('/(?:^|\.)((?:not ok|dubious|errors) *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'ERROR\')', $message);
688
      $message = preg_replace('/(?:^|\.)(ok *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'INFO\')', $message);
689
      $message = preg_replace('/"(.+?)"/e', '$this->colorizer->colorize(\'$1\', \'PARAMETER\')', $message);
690
      $message = preg_replace('/(\->|\:\:)?([a-zA-Z0-9_]+?)\(\)/e', '$this->colorizer->colorize(\'$1$2()\', \'PARAMETER\')', $message);
691
    }
692
 
693
    echo ($colorizer_parameter ? $this->colorizer->colorize($message, $colorizer_parameter) : $message)."\n";
694
  }
695
 
696
  public function green_bar($message)
697
  {
698
    echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'GREEN_BAR')."\n";
699
  }
700
 
701
  public function red_bar($message)
702
  {
703
    echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'RED_BAR')."\n";
704
  }
705
 
706
  protected function strip_base_dir($text)
707
  {
708
    return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(realpath($this->base_dir).DIRECTORY_SEPARATOR, '', $text));
709
  }
710
}
711
 
712
class lime_output_color extends lime_output
713
{
714
}
715
 
716
class lime_colorizer
717
{
718
  static public $styles = array();
719
 
720
  protected $colors_supported = false;
721
 
722
  public function __construct($force_colors = false)
723
  {
724
    if ($force_colors)
725
    {
726
      $this->colors_supported = true;
727
    }
728
    else
729
    {
730
      // colors are supported on windows with ansicon or on tty consoles
731
      if (DIRECTORY_SEPARATOR == '\\')
732
      {
733
        $this->colors_supported = false !== getenv('ANSICON');
734
      }
735
      else
736
      {
737
        $this->colors_supported = function_exists('posix_isatty') && @posix_isatty(STDOUT);
738
      }
739
    }
740
  }
741
 
742
  public static function style($name, $options = array())
743
  {
744
    self::$styles[$name] = $options;
745
  }
746
 
747
  public function colorize($text = '', $parameters = array())
748
  {
749
 
750
    if (!$this->colors_supported)
751
    {
752
      return $text;
753
    }
754
 
755
    static $options    = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8);
756
    static $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37);
757
    static $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47);
758
 
759
    !is_array($parameters) && isset(self::$styles[$parameters]) and $parameters = self::$styles[$parameters];
760
 
761
    $codes = array();
762
    isset($parameters['fg']) and $codes[] = $foreground[$parameters['fg']];
763
    isset($parameters['bg']) and $codes[] = $background[$parameters['bg']];
764
    foreach ($options as $option => $value)
765
    {
766
      isset($parameters[$option]) && $parameters[$option] and $codes[] = $value;
767
    }
768
 
769
    return "\033[".implode(';', $codes).'m'.$text."\033[0m";
770
  }
771
}
772
 
773
lime_colorizer::style('ERROR', array('bg' => 'red', 'fg' => 'white', 'bold' => true));
774
lime_colorizer::style('INFO', array('fg' => 'green', 'bold' => true));
775
lime_colorizer::style('TRACE', array('fg' => 'green', 'bold' => true));
776
lime_colorizer::style('PARAMETER', array('fg' => 'cyan'));
777
lime_colorizer::style('COMMENT', array('fg' => 'yellow'));
778
 
779
lime_colorizer::style('GREEN_BAR', array('fg' => 'white', 'bg' => 'green', 'bold' => true));
780
lime_colorizer::style('RED_BAR', array('fg' => 'white', 'bg' => 'red', 'bold' => true));
781
lime_colorizer::style('INFO_BAR', array('fg' => 'cyan', 'bold' => true));
782
 
783
class lime_harness extends lime_registration
784
{
785
  public $options = array();
786
  public $php_cli = null;
787
  public $stats   = array();
788
  public $output  = null;
789
 
790
  public function __construct($options = array())
791
  {
792
    // for BC
793
    if (!is_array($options))
794
    {
795
      $options = array('output' => $options);
796
    }
797
 
798
    $this->options = array_merge(array(
799
      'php_cli'      => null,
800
      'force_colors' => false,
801
      'output'       => null,
802
      'verbose'      => false,
803
    ), $options);
804
 
805
    $this->php_cli = $this->find_php_cli($this->options['php_cli']);
806
    $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']);
807
  }
808
 
809
  protected function find_php_cli($php_cli = null)
810
  {
811
    if (is_null($php_cli))
812
    {
813
      if (getenv('PHP_PATH'))
814
      {
815
        $php_cli = getenv('PHP_PATH');
816
 
817
        if (!is_executable($php_cli))
818
        {
819
          throw new Exception('The defined PHP_PATH environment variable is not a valid PHP executable.');
820
        }
821
      }
822
      else
823
      {
824
        $php_cli = PHP_BINDIR.DIRECTORY_SEPARATOR.'php';
825
      }
826
    }
827
 
828
    if (is_executable($php_cli))
829
    {
830
      return $php_cli;
831
    }
832
 
833
    $path = getenv('PATH') ? getenv('PATH') : getenv('Path');
834
    $exe_suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array('');
835
    foreach (array('php5', 'php') as $php_cli)
836
    {
837
      foreach ($exe_suffixes as $suffix)
838
      {
839
        foreach (explode(PATH_SEPARATOR, $path) as $dir)
840
        {
841
          $file = $dir.DIRECTORY_SEPARATOR.$php_cli.$suffix;
842
          if (is_executable($file))
843
          {
844
            return $file;
845
          }
846
        }
847
      }
848
    }
849
 
850
    throw new Exception("Unable to find PHP executable.");
851
  }
852
 
853
  public function to_array()
854
  {
855
    $results = array();
856
    foreach ($this->stats['files'] as $file => $stat)
857
    {
858
      $results = array_merge($results, $stat['output']);
859
    }
860
 
861
    return $results;
862
  }
863
 
864
  public function to_xml()
865
  {
866
    return lime_test::to_xml($this->to_array());
867
  }
868
 
869
  public function run()
870
  {
871
    if (!count($this->files))
872
    {
873
      throw new Exception('You must register some test files before running them!');
874
    }
875
 
876
    // sort the files to be able to predict the order
877
    sort($this->files);
878
 
879
    $this->stats = array(
880
      'files'        => array(),
881
      'failed_files' => array(),
882
      'failed_tests' => 0,
883
      'total'        => 0,
884
    );
885
 
886
    foreach ($this->files as $file)
887
    {
888
      $this->stats['files'][$file] = array();
889
      $stats = &$this->stats['files'][$file];
890
 
891
      $relative_file = $this->get_relative_file($file);
892
 
893
      $test_file = tempnam(sys_get_temp_dir(), 'lime');
894
      $result_file = tempnam(sys_get_temp_dir(), 'lime');
895
      file_put_contents($test_file, <<<EOF
896
<?php
897
function lime_shutdown()
898
{
899
  file_put_contents('$result_file', serialize(lime_test::to_array()));
900
}
901
register_shutdown_function('lime_shutdown');
902
include('$file');
903
EOF
904
      );
905
 
906
      ob_start();
907
      // see http://trac.symfony-project.org/ticket/5437 for the explanation on the weird "cd" thing
908
      passthru(sprintf('cd & %s %s 2>&1', escapeshellarg($this->php_cli), escapeshellarg($test_file)), $return);
909
      ob_end_clean();
910
      unlink($test_file);
911
 
912
      $output = file_get_contents($result_file);
913
      $stats['output'] = $output ? unserialize($output) : '';
914
      if (!$stats['output'])
915
      {
916
        $stats['output'] = array(array('file' => $file, 'tests' => array(), 'stats' => array('plan' => 1, 'total' => 1, 'failed' => array(0), 'passed' => array(), 'skipped' => array(), 'errors' => array())));
917
      }
918
      unlink($result_file);
919
 
920
      $file_stats = &$stats['output'][0]['stats'];
921
 
922
      $delta = 0;
923
      if ($return > 0)
924
      {
925
        $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious';
926
        $stats['status_code'] = $return;
927
      }
928
      else
929
      {
930
        $this->stats['total'] += $file_stats['total'];
931
 
932
        if (!$file_stats['plan'])
933
        {
934
          $file_stats['plan'] = $file_stats['total'];
935
        }
936
 
937
        $delta = $file_stats['plan'] - $file_stats['total'];
938
        if (0 != $delta)
939
        {
940
          $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious';
941
          $stats['status_code'] = 255;
942
        }
943
        else
944
        {
945
          $stats['status'] = $file_stats['failed'] ? 'not ok' : ($file_stats['errors'] ? 'errors' : 'ok');
946
          $stats['status_code'] = 0;
947
        }
948
      }
949
 
950
      $this->output->echoln(sprintf('%s%s%s', substr($relative_file, -min(67, strlen($relative_file))), str_repeat('.', 70 - min(67, strlen($relative_file))), $stats['status']));
951
 
952
      if ('dubious' == $stats['status'])
953
      {
954
        $this->output->echoln(sprintf('    Test returned status %s', $stats['status_code']));
955
      }
956
 
957
      if ('ok' != $stats['status'])
958
      {
959
        $this->stats['failed_files'][] = $file;
960
      }
961
 
962
      if ($delta > 0)
963
      {
964
        $this->output->echoln(sprintf('    Looks like you planned %d tests but only ran %d.', $file_stats['plan'], $file_stats['total']));
965
 
966
        $this->stats['failed_tests'] += $delta;
967
        $this->stats['total'] += $delta;
968
      }
969
      else if ($delta < 0)
970
      {
971
        $this->output->echoln(sprintf('    Looks like you planned %s test but ran %s extra.', $file_stats['plan'], $file_stats['total'] - $file_stats['plan']));
972
      }
973
 
974
      if (false !== $file_stats && $file_stats['failed'])
975
      {
976
        $this->stats['failed_tests'] += count($file_stats['failed']);
977
 
978
        $this->output->echoln(sprintf("    Failed tests: %s", implode(', ', $file_stats['failed'])));
979
      }
980
 
981
      if (false !== $file_stats && $file_stats['errors'])
982
      {
983
        $this->output->echoln('    Errors:');
984
 
985
        $error_count = count($file_stats['errors']);
986
        for ($i = 0; $i < 3 && $i < $error_count; ++$i)
987
        {
988
          $this->output->echoln('    - ' . $file_stats['errors'][$i]['message'], null, false);
989
        }
990
        if ($error_count > 3)
991
        {
992
          $this->output->echoln(sprintf('    ... and %s more', $error_count-3));
993
        }
994
      }
995
    }
996
 
997
    if (count($this->stats['failed_files']))
998
    {
999
      $format = "%-30s  %4s  %5s  %5s  %5s  %s";
1000
      $this->output->echoln(sprintf($format, 'Failed Test', 'Stat', 'Total', 'Fail', 'Errors', 'List of Failed'));
1001
      $this->output->echoln("--------------------------------------------------------------------------");
1002
      foreach ($this->stats['files'] as $file => $stat)
1003
      {
1004
        if (!in_array($file, $this->stats['failed_files']))
1005
        {
1006
          continue;
1007
        }
1008
        $relative_file = $this->get_relative_file($file);
1009
 
1010
        if (isset($stat['output'][0]))
1011
        {
1012
          $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], count($stat['output'][0]['stats']['failed']) + count($stat['output'][0]['stats']['passed']), count($stat['output'][0]['stats']['failed']), count($stat['output'][0]['stats']['errors']), implode(' ', $stat['output'][0]['stats']['failed'])));
1013
        }
1014
        else
1015
        {
1016
          $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], '', '', ''));
1017
        }
1018
      }
1019
 
1020
      $this->output->red_bar(sprintf('Failed %d/%d test scripts, %.2f%% okay. %d/%d subtests failed, %.2f%% okay.',
1021
        $nb_failed_files = count($this->stats['failed_files']),
1022
        $nb_files = count($this->files),
1023
        ($nb_files - $nb_failed_files) * 100 / $nb_files,
1024
        $nb_failed_tests = $this->stats['failed_tests'],
1025
        $nb_tests = $this->stats['total'],
1026
        $nb_tests > 0 ? ($nb_tests - $nb_failed_tests) * 100 / $nb_tests : 0
1027
      ));
1028
 
1029
      if ($this->options['verbose'])
1030
      {
1031
        foreach ($this->to_array() as $testsuite)
1032
        {
1033
          $first = true;
1034
          foreach ($testsuite['stats']['failed'] as $testcase)
1035
          {
1036
            if (!isset($testsuite['tests'][$testcase]['file']))
1037
            {
1038
              continue;
1039
            }
1040
 
1041
            if ($first)
1042
            {
1043
              $this->output->echoln('');
1044
              $this->output->error($this->get_relative_file($testsuite['file']).$this->extension);
1045
              $first = false;
1046
            }
1047
 
1048
            $this->output->comment(sprintf('  at %s line %s', $this->get_relative_file($testsuite['tests'][$testcase]['file']).$this->extension, $testsuite['tests'][$testcase]['line']));
1049
            $this->output->info('  '.$testsuite['tests'][$testcase]['message']);
1050
            $this->output->echoln($testsuite['tests'][$testcase]['error'], null, false);
1051
          }
1052
        }
1053
      }
1054
    }
1055
    else
1056
    {
1057
      $this->output->green_bar(' All tests successful.');
1058
      $this->output->green_bar(sprintf(' Files=%d, Tests=%d', count($this->files), $this->stats['total']));
1059
    }
1060
 
1061
    return $this->stats['failed_files'] ? false : true;
1062
  }
1063
 
1064
  public function get_failed_files()
1065
  {
1066
    return isset($this->stats['failed_files']) ? $this->stats['failed_files'] : array();
1067
  }
1068
}
1069
 
1070
class lime_coverage extends lime_registration
1071
{
1072
  public $files = array();
1073
  public $extension = '.php';
1074
  public $base_dir = '';
1075
  public $harness = null;
1076
  public $verbose = false;
1077
  protected $coverage = array();
1078
 
1079
  public function __construct($harness)
1080
  {
1081
    $this->harness = $harness;
1082
 
1083
    if (!function_exists('xdebug_start_code_coverage'))
1084
    {
1085
      throw new Exception('You must install and enable xdebug before using lime coverage.');
1086
    }
1087
 
1088
    if (!ini_get('xdebug.extended_info'))
1089
    {
1090
      throw new Exception('You must set xdebug.extended_info to 1 in your php.ini to use lime coverage.');
1091
    }
1092
  }
1093
 
1094
  public function run()
1095
  {
1096
    if (!count($this->harness->files))
1097
    {
1098
      throw new Exception('You must register some test files before running coverage!');
1099
    }
1100
 
1101
    if (!count($this->files))
1102
    {
1103
      throw new Exception('You must register some files to cover!');
1104
    }
1105
 
1106
    $this->coverage = array();
1107
 
1108
    $this->process($this->harness->files);
1109
 
1110
    $this->output($this->files);
1111
  }
1112
 
1113
  public function process($files)
1114
  {
1115
    if (!is_array($files))
1116
    {
1117
      $files = array($files);
1118
    }
1119
 
1120
    $tmp_file = sys_get_temp_dir().DIRECTORY_SEPARATOR.'test.php';
1121
    foreach ($files as $file)
1122
    {
1123
      $tmp = <<<EOF
1124
<?php
1125
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
1126
include('$file');
1127
echo '<PHP_SER>'.serialize(xdebug_get_code_coverage()).'</PHP_SER>';
1128
EOF;
1129
      file_put_contents($tmp_file, $tmp);
1130
      ob_start();
1131
      // see http://trac.symfony-project.org/ticket/5437 for the explanation on the weird "cd" thing
1132
      passthru(sprintf('cd & %s %s 2>&1', escapeshellarg($this->harness->php_cli), escapeshellarg($tmp_file)), $return);
1133
      $retval = ob_get_clean();
1134
 
1135
      if (0 != $return) // test exited without success
1136
      {
1137
        // something may have gone wrong, we should warn the user so they know
1138
        // it's a bug in their code and not symfony's
1139
 
1140
        $this->harness->output->echoln(sprintf('Warning: %s returned status %d, results may be inaccurate', $file, $return), 'ERROR');
1141
      }
1142
 
1143
      if (false === $cov = @unserialize(substr($retval, strpos($retval, '<PHP_SER>') + 9, strpos($retval, '</PHP_SER>') - 9)))
1144
      {
1145
        if (0 == $return)
1146
        {
1147
          // failed to serialize, but PHP said it should of worked.
1148
          // something is seriously wrong, so abort with exception
1149
          throw new Exception(sprintf('Unable to unserialize coverage for file "%s"', $file));
1150
        }
1151
        else
1152
        {
1153
          // failed to serialize, but PHP warned us that this might have happened.
1154
          // so we should ignore and move on
1155
          continue; // continue foreach loop through $this->harness->files
1156
        }
1157
      }
1158
 
1159
      foreach ($cov as $file => $lines)
1160
      {
1161
        if (!isset($this->coverage[$file]))
1162
        {
1163
          $this->coverage[$file] = $lines;
1164
          continue;
1165
        }
1166
 
1167
        foreach ($lines as $line => $flag)
1168
        {
1169
          if ($flag == 1)
1170
          {
1171
            $this->coverage[$file][$line] = 1;
1172
          }
1173
        }
1174
      }
1175
    }
1176
 
1177
    if (file_exists($tmp_file))
1178
    {
1179
      unlink($tmp_file);
1180
    }
1181
  }
1182
 
1183
  public function output($files)
1184
  {
1185
    ksort($this->coverage);
1186
    $total_php_lines = 0;
1187
    $total_covered_lines = 0;
1188
    foreach ($files as $file)
1189
    {
1190
      $file = realpath($file);
1191
      $is_covered = isset($this->coverage[$file]);
1192
      $cov = isset($this->coverage[$file]) ? $this->coverage[$file] : array();
1193
      $covered_lines = array();
1194
      $missing_lines = array();
1195
 
1196
      foreach ($cov as $line => $flag)
1197
      {
1198
        switch ($flag)
1199
        {
1200
          case 1:
1201
            $covered_lines[] = $line;
1202
            break;
1203
          case -1:
1204
            $missing_lines[] = $line;
1205
            break;
1206
        }
1207
      }
1208
 
1209
      $total_lines = count($covered_lines) + count($missing_lines);
1210
      if (!$total_lines)
1211
      {
1212
        // probably means that the file is not covered at all!
1213
        $total_lines = count($this->get_php_lines(file_get_contents($file)));
1214
      }
1215
 
1216
      $output = $this->harness->output;
1217
      $percent = $total_lines ? count($covered_lines) * 100 / $total_lines : 0;
1218
 
1219
      $total_php_lines += $total_lines;
1220
      $total_covered_lines += count($covered_lines);
1221
 
1222
      $relative_file = $this->get_relative_file($file);
1223
      $output->echoln(sprintf("%-70s %3.0f%%", substr($relative_file, -min(70, strlen($relative_file))), $percent), $percent == 100 ? 'INFO' : ($percent > 90 ? 'PARAMETER' : ($percent < 20 ? 'ERROR' : '')));
1224
      if ($this->verbose && $is_covered && $percent != 100)
1225
      {
1226
        $output->comment(sprintf("missing: %s", $this->format_range($missing_lines)));
1227
      }
1228
    }
1229
 
1230
    $output->echoln(sprintf("TOTAL COVERAGE: %3.0f%%", $total_php_lines ? $total_covered_lines * 100 / $total_php_lines : 0));
1231
  }
1232
 
1233
  public static function get_php_lines($content)
1234
  {
1235
    if (is_readable($content))
1236
    {
1237
      $content = file_get_contents($content);
1238
    }
1239
 
1240
    $tokens = token_get_all($content);
1241
    $php_lines = array();
1242
    $current_line = 1;
1243
    $in_class = false;
1244
    $in_function = false;
1245
    $in_function_declaration = false;
1246
    $end_of_current_expr = true;
1247
    $open_braces = 0;
1248
    foreach ($tokens as $token)
1249
    {
1250
      if (is_string($token))
1251
      {
1252
        switch ($token)
1253
        {
1254
          case '=':
1255
            if (false === $in_class || (false !== $in_function && !$in_function_declaration))
1256
            {
1257
              $php_lines[$current_line] = true;
1258
            }
1259
            break;
1260
          case '{':
1261
            ++$open_braces;
1262
            $in_function_declaration = false;
1263
            break;
1264
          case ';':
1265
            $in_function_declaration = false;
1266
            $end_of_current_expr = true;
1267
            break;
1268
          case '}':
1269
            $end_of_current_expr = true;
1270
            --$open_braces;
1271
            if ($open_braces == $in_class)
1272
            {
1273
              $in_class = false;
1274
            }
1275
            if ($open_braces == $in_function)
1276
            {
1277
              $in_function = false;
1278
            }
1279
            break;
1280
        }
1281
 
1282
        continue;
1283
      }
1284
 
1285
      list($id, $text) = $token;
1286
 
1287
      switch ($id)
1288
      {
1289
        case T_CURLY_OPEN:
1290
        case T_DOLLAR_OPEN_CURLY_BRACES:
1291
          ++$open_braces;
1292
          break;
1293
        case T_WHITESPACE:
1294
        case T_OPEN_TAG:
1295
        case T_CLOSE_TAG:
1296
          $end_of_current_expr = true;
1297
          $current_line += count(explode("\n", $text)) - 1;
1298
          break;
1299
        case T_COMMENT:
1300
        case T_DOC_COMMENT:
1301
          $current_line += count(explode("\n", $text)) - 1;
1302
          break;
1303
        case T_CLASS:
1304
          $in_class = $open_braces;
1305
          break;
1306
        case T_FUNCTION:
1307
          $in_function = $open_braces;
1308
          $in_function_declaration = true;
1309
          break;
1310
        case T_AND_EQUAL:
1311
        case T_BREAK:
1312
        case T_CASE:
1313
        case T_CATCH:
1314
        case T_CLONE:
1315
        case T_CONCAT_EQUAL:
1316
        case T_CONTINUE:
1317
        case T_DEC:
1318
        case T_DECLARE:
1319
        case T_DEFAULT:
1320
        case T_DIV_EQUAL:
1321
        case T_DO:
1322
        case T_ECHO:
1323
        case T_ELSEIF:
1324
        case T_EMPTY:
1325
        case T_ENDDECLARE:
1326
        case T_ENDFOR:
1327
        case T_ENDFOREACH:
1328
        case T_ENDIF:
1329
        case T_ENDSWITCH:
1330
        case T_ENDWHILE:
1331
        case T_EVAL:
1332
        case T_EXIT:
1333
        case T_FOR:
1334
        case T_FOREACH:
1335
        case T_GLOBAL:
1336
        case T_IF:
1337
        case T_INC:
1338
        case T_INCLUDE:
1339
        case T_INCLUDE_ONCE:
1340
        case T_INSTANCEOF:
1341
        case T_ISSET:
1342
        case T_IS_EQUAL:
1343
        case T_IS_GREATER_OR_EQUAL:
1344
        case T_IS_IDENTICAL:
1345
        case T_IS_NOT_EQUAL:
1346
        case T_IS_NOT_IDENTICAL:
1347
        case T_IS_SMALLER_OR_EQUAL:
1348
        case T_LIST:
1349
        case T_LOGICAL_AND:
1350
        case T_LOGICAL_OR:
1351
        case T_LOGICAL_XOR:
1352
        case T_MINUS_EQUAL:
1353
        case T_MOD_EQUAL:
1354
        case T_MUL_EQUAL:
1355
        case T_NEW:
1356
        case T_OBJECT_OPERATOR:
1357
        case T_OR_EQUAL:
1358
        case T_PLUS_EQUAL:
1359
        case T_PRINT:
1360
        case T_REQUIRE:
1361
        case T_REQUIRE_ONCE:
1362
        case T_RETURN:
1363
        case T_SL:
1364
        case T_SL_EQUAL:
1365
        case T_SR:
1366
        case T_SR_EQUAL:
1367
        case T_SWITCH:
1368
        case T_THROW:
1369
        case T_TRY:
1370
        case T_UNSET:
1371
        case T_UNSET_CAST:
1372
        case T_USE:
1373
        case T_WHILE:
1374
        case T_XOR_EQUAL:
1375
          $php_lines[$current_line] = true;
1376
          $end_of_current_expr = false;
1377
          break;
1378
        default:
1379
          if (false === $end_of_current_expr)
1380
          {
1381
            $php_lines[$current_line] = true;
1382
          }
1383
      }
1384
    }
1385
 
1386
    return $php_lines;
1387
  }
1388
 
1389
  public function compute($content, $cov)
1390
  {
1391
    $php_lines = self::get_php_lines($content);
1392
 
1393
    // we remove from $cov non php lines
1394
    foreach (array_diff_key($cov, $php_lines) as $line => $tmp)
1395
    {
1396
      unset($cov[$line]);
1397
    }
1398
 
1399
    return array($cov, $php_lines);
1400
  }
1401
 
1402
  public function format_range($lines)
1403
  {
1404
    sort($lines);
1405
    $formatted = '';
1406
    $first = -1;
1407
    $last = -1;
1408
    foreach ($lines as $line)
1409
    {
1410
      if ($last + 1 != $line)
1411
      {
1412
        if ($first != -1)
1413
        {
1414
          $formatted .= $first == $last ? "$first " : "[$first - $last] ";
1415
        }
1416
        $first = $line;
1417
        $last = $line;
1418
      }
1419
      else
1420
      {
1421
        $last = $line;
1422
      }
1423
    }
1424
    if ($first != -1)
1425
    {
1426
      $formatted .= $first == $last ? "$first " : "[$first - $last] ";
1427
    }
1428
 
1429
    return $formatted;
1430
  }
1431
}
1432
 
1433
class lime_registration
1434
{
1435
  public $files = array();
1436
  public $extension = '.php';
1437
  public $base_dir = '';
1438
 
1439
  public function register($files_or_directories)
1440
  {
1441
    foreach ((array) $files_or_directories as $f_or_d)
1442
    {
1443
      if (is_file($f_or_d))
1444
      {
1445
        $this->files[] = realpath($f_or_d);
1446
      }
1447
      elseif (is_dir($f_or_d))
1448
      {
1449
        $this->register_dir($f_or_d);
1450
      }
1451
      else
1452
      {
1453
        throw new Exception(sprintf('The file or directory "%s" does not exist.', $f_or_d));
1454
      }
1455
    }
1456
  }
1457
 
1458
  public function register_glob($glob)
1459
  {
1460
    if ($dirs = glob($glob))
1461
    {
1462
      foreach ($dirs as $file)
1463
      {
1464
        $this->files[] = realpath($file);
1465
      }
1466
    }
1467
  }
1468
 
1469
  public function register_dir($directory)
1470
  {
1471
    if (!is_dir($directory))
1472
    {
1473
      throw new Exception(sprintf('The directory "%s" does not exist.', $directory));
1474
    }
1475
 
1476
    $files = array();
1477
 
1478
    $current_dir = opendir($directory);
1479
    while ($entry = readdir($current_dir))
1480
    {
1481
      if ($entry == '.' || $entry == '..') continue;
1482
 
1483
      if (is_dir($entry))
1484
      {
1485
        $this->register_dir($entry);
1486
      }
1487
      elseif (preg_match('#'.$this->extension.'$#', $entry))
1488
      {
1489
        $files[] = realpath($directory.DIRECTORY_SEPARATOR.$entry);
1490
      }
1491
    }
1492
 
1493
    $this->files = array_merge($this->files, $files);
1494
  }
1495
 
1496
  protected function get_relative_file($file)
1497
  {
1498
    return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(array(realpath($this->base_dir).DIRECTORY_SEPARATOR, $this->extension), '', $file));
1499
  }
1500
}