<?php

/**

* Prints tests' results in a similar way
* to rspec's progress formatter.
*
* @package    PHPUnit
* @subpackage Progress
* @author     Maher Sallam <maher@sallam.me>
* @copyright  2011 Maher Sallam <maher@sallam.me>
* @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
* @version    0.1
*/

class PHPUnit_Extensions_Progress_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {

/**
 * Constructor.
 *
 * @param  mixed   $out
 * @param  boolean $verbose
 * @param  boolean $colors
 * @param  boolean $debug
 */
public function __construct($out = NULL, $verbose = FALSE, $colors = FALSE, $debug = FALSE) {

  // Start capturing output
  ob_start();

  $argv = $_SERVER['argv'];
  $colors  = in_array('--colors', $argv) || $colors;
  $verbose = in_array('--verbose', $argv) || in_array('-v', $argv) || $verbose;
  $debug   = in_array('--debug', $argv) || $debug;

  parent::__construct($out, $verbose, $colors, $debug);
}

/**
 * @param  PHPUnit_Framework_TestResult $result
 */
public function printResult(PHPUnit_Framework_TestResult $result)
{
  print "\n";

  if ($result->errorCount() > 0) {
    $this->printErrors($result);
  }

  if ($result->failureCount() > 0) {
    $this->printFailures($result);
  }

  if ($this->verbose) {
    if (method_exists($result, 'deprecatedFeaturesCount')) {
      if ($result->deprecatedFeaturesCount() > 0) {
        if ($result->failureCount() > 0) {
          print "\n--\n\nDeprecated PHPUnit features are being used";
        }

        foreach ($result->deprecatedFeatures() as $deprecatedFeature) {
          $this->write($deprecatedFeature . "\n\n");
        }
      }
    }

    if ($result->notImplementedCount() > 0) {
      $this->printIncompletes($result);
    }

    if ($result->skippedCount() > 0) {
      $this->printSkipped($result);
    }
  }

  $this->printFooter($result);
} 

/**
 * @param  array   $defects
 * @param  string  $type
 */
protected function printDefects(array $defects, $type)
{
  $count = count($defects);

  if ($count == 0) {
    return;
  }

  $this->write("\n" . $type . ":\n");

  $i = 1;
  $failOrError = $type == 'Failures' || $type == 'Errors';

  foreach ($defects as $defect) {
    $this->printDefect($defect, $i++, $failOrError);
    $this->write("\n");
  }
}

/**
 * @param  PHPUnit_Framework_TestFailure $defect
 * @param  integer                       $count
 * @param  boolean                       $failOrError
 */
protected function printDefect(PHPUnit_Framework_TestFailure $defect, $count, $failOrError = true)
{
  $this->printDefectHeader($defect, $count, $failOrError);

  $padding = str_repeat(' ', 
    4 + ( $failOrError ? strlen((string)$count) : 0 )
  );

  $this->printDefectBody($defect, $count, $failOrError, $padding);
  $this->printDefectTrace($defect, $padding);
}

/**
 * @param  PHPUnit_Framework_TestFailure $defect
 * @param  integer                       $count
 * @param  boolean                       $failOrError
 */
protected function printDefectHeader(PHPUnit_Framework_TestFailure $defect, $count, $failOrError = true)
{
  $failedTest = $defect->failedTest();

  if ($failedTest instanceof PHPUnit_Framework_SelfDescribing) {
    $testName = $failedTest->toString();
  } else {
    $testName = get_class($failedTest);
  }

  if ( $failOrError ) {
    $this->write(
      sprintf(
        "\n  %d) %s",

        $count,
        $testName
      )
    );
  } else {
    $this->write( 
      sprintf( "  %s", $this->yellow($testName) )
    ); 
  }
}

/**
 * @param  PHPUnit_Framework_TestFailure $defect
 * @param  integer                       $count
 * @param  boolean                       $failOrError
 * @param  string                        $padding
 */
protected function printDefectBody(PHPUnit_Framework_TestFailure $defect, $count, $failOrError, $padding)
{
  $error = trim($defect->getExceptionAsString());

  if ( !empty($error) ) {
    $error = explode("\n", $error);
    $error = "\n" . $padding . implode("\n  " . $padding , $error);

    $this->write( $failOrError ? $this->red($error) : $this->cyan($error) );
  }
}

/**
 * @param  PHPUnit_Framework_TestFailure $defect
 * @param  string                        $padding
 */
protected function printDefectTrace(PHPUnit_Framework_TestFailure $defect, $padding = 0)
{ 
  $trace = trim(
    PHPUnit_Util_Filter::getFilteredStacktrace(
      $defect->thrownException()
    )
  );

  if ( ! empty($trace) ) {
    $trace = explode("\n", $trace);
    $trace = "\n" . $padding . '# ' . implode("\n${padding}# ", $trace);

    $this->write($this->cyan($trace));
  }
}

/**
 * @param  PHPUnit_Framework_TestResult  $result
 */
protected function printErrors(PHPUnit_Framework_TestResult $result)
{
  $this->printDefects(
    $result->errors(),
    $result->errorCount(), 
    'Errors'
  );
}

/**
 * @param  PHPUnit_Framework_TestResult  $result
 */
protected function printFailures(PHPUnit_Framework_TestResult $result)
{
  $this->printDefects(
    $result->failures(),
    $result->failureCount(),
    'Failures'
  );
}

/**
 * @param  PHPUnit_Framework_TestResult  $result
 */
protected function printIncompletes(PHPUnit_Framework_TestResult $result)
{
  $this->printDefects(
    $result->notImplemented(),
    $result->notImplementedCount(),
    'Incomplete tests'
  );
}

/**
 * @param  PHPUnit_Framework_TestResult  $result
 * @since  Method available since Release 3.0.0
 */
protected function printSkipped(PHPUnit_Framework_TestResult $result)
{
  $this->printDefects(
    $result->skipped(),
    $result->skippedCount(),
    'Skipped tests'
  );
}

/**
 * @param  PHPUnit_Framework_TestResult  $result
 */
protected function printFooter(PHPUnit_Framework_TestResult $result)
{

  $this->write( sprintf("\nFinished in %s\n", PHP_Timer::timeSinceStartOfRequest()) );

  $resultsCount = count($result);

  $footer = sprintf("%d test%s, %d assertion%s",
    $resultsCount,
    $resultsCount == 1 ? '' : 's',
    $this->numAssertions,
    $this->numAssertions == 1 ? '' : 's' 
  );

  // backwards/forwards compatibility hack for naming fix from phpunit 3.7.11
  // @see https://github.com/sebastianbergmann/phpunit/issues/762
  $allCompletelyImplemented = method_exists($result, 'allCompletelyImplemented')?
        'allCompletelyImplemented':'allCompletlyImplemented';

  if ( $result->wasSuccessful() &&
        $result->$allCompletelyImplemented() &&
    $result->noneSkipped() )
  {
    $this->write($this->green($footer));
  }

  else if ( ( !$result->$allCompletelyImplemented() || !$result->noneSkipped() ) 
    &&
    $result->wasSuccessful() ) 
  {

    $footer .= sprintf(
      "%s%s",

      $this->getCountString(
        $result->notImplementedCount(), 'incomplete'
      ),
      $this->getCountString(
        $result->skippedCount(), 'skipped'
      )
    );

    $this->write($this->yellow($footer));

  }

  else {

    $footer .= sprintf(
      "%s%s%s%s",

      $this->getCountString($result->failureCount(), 'failures'),
      $this->getCountString($result->errorCount(), 'errors'),
      $this->getCountString(
        $result->notImplementedCount(), 'incomplete'
      ),
      $this->getCountString($result->skippedCount(), 'skipped')
    );

    $footer  = preg_replace('/,$/', '', $footer);

    $this->write($this->red($footer));
  }

  if (method_exists($result, 'deprecatedFeaturesCount')) {
    if ( ! $this->verbose &&
      $result->deprecatedFeaturesCount() > 0 )
    {
      $message = sprintf(
        "Warning: Deprecated PHPUnit features are being used %s times!\n".
        "Use --verbose for more information.\n",
        $result->deprecatedFeaturesCount()
      );

      if ($this->colors) {
        $message = "\x1b[37;41m\x1b[2K" . $message .
          "\x1b[0m";
      }

      $this->write("\n" . $message);
    }
  }

  $this->writeNewLine();
}

/**
 * @param  integer $count
 * @param  string  $name
 * @return string
 * @since  Method available since Release 3.0.0
 */
protected function getCountString($count, $name)
{
  $string = '';

  if ($count > 0) {
    $string = sprintf(
      ', %d %s',

      $count,
      $name
    );
  }

  return $string;
}

/**
 * An error occurred.
 *
 * @param  PHPUnit_Framework_Test $test
 * @param  Exception              $e
 * @param  float                  $time
 */
public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
{
  $this->writeProgress($this->red('E'));
  $this->lastTestFailed = TRUE;
}

/**
 * A failure occurred.
 *
 * @param  PHPUnit_Framework_Test                 $test
 * @param  PHPUnit_Framework_AssertionFailedError $e
 * @param  float                                  $time
 */
public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
{
  $this->writeProgress($this->red('F'));
  $this->lastTestFailed = TRUE;
}

/**
 * Incomplete test.
 *
 * @param  PHPUnit_Framework_Test $test
 * @param  Exception              $e
 * @param  float                  $time
 */
public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
  $this->writeProgress($this->yellow('I'));
  $this->lastTestFailed = TRUE;
}

/**
 * Skipped test.
 *
 * @param  PHPUnit_Framework_Test $test
 * @param  Exception              $e
 * @param  float                  $time
 * @since  Method available since Release 3.0.0
 */
public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
  $this->writeProgress($this->yellow('S'));
  $this->lastTestFailed = TRUE;
}

/**
 * A test ended.
 *
 * @param  PHPUnit_Framework_Test $test
 * @param  float                  $time
 */
public function endTest(PHPUnit_Framework_Test $test, $time)
{
  if (!$this->lastTestFailed) {
    $this->writeProgress($this->green('.'));
  }

  if ($test instanceof PHPUnit_Framework_TestCase) {
    $this->numAssertions += $test->getNumAssertions();
  }

  else if ($test instanceof PHPUnit_Extensions_PhptTestCase) {
    $this->numAssertions++;
  }

  $this->lastTestFailed = FALSE;

  if ($this->verbose && $test instanceof PHPUnit_Framework_TestCase) {
    $this->write($test->getActualOutput());
  }

}

/**
 * @param  string $progress
 */
protected function writeProgress($progress)
{
  static $deletedHeader = false;

  if ( ! $deletedHeader ) {
    if (ob_get_length() > 0) {
      ob_end_clean();
      $deletedHeader = true;
    }
  }

  parent::writeProgress($progress);
}

/**
 * Returns a colored string which can be used
 * in the terminal.
 *
 * @param string  $text
 * @param integer $color_code
 */
protected function color($text, $color_code) {
  return $this->colors ? "\033[${color_code}m" . $text . "\033[0m" : $text;
}

/**
 * @param string $text
 */
protected function bold($text) {
  return $this->color($text, "1");
}

/**
 * @param string $text
 */
protected function red($text) {
  return $this->color($text, "31");
}

/**
 * @param string $text
 */
protected function green($text) {
  return $this->color($text, "32");
}

/**
 * @param string $text
 */
protected function yellow($text) {
  return $this->color($text, "33");
}

/**
 * @param string $text
 */
protected function blue($text) {
  return $this->color($text, "34");
}

/**
 * @param string $text
 */
protected function magenta($text) {
  return $this->color($text, "35");
}

/**
 * @param string $text
 */
protected function cyan($text) {
  return $this->color($text, "36");
}

/**
 * @param string $text
 */
protected function white($text) {
  return $this->color($text, "37");
}

}