<?php

/**
 * A PHPUnit TestListener that exposes your slowest running tests by outputting
 * results directly to the console.
 */
class SpeedTrapListener implements PHPUnit_Framework_TestListener {

	/**
	 * Internal tracking for test suites.
	 *
	 * Increments as more suites are run, then decremented as they finish. All
	 * suites have been run when returns to 0.
	 *
	 * @var integer
	 */
	protected $suites = 0;

	/**
	 * Time in milliseconds at which a test will be considered "slow" and be
	 * reported by this listener.
	 *
	 * @var int
	 */
	protected $slow_threshold;

	/**
	 * Number of tests to report on for slowness.
	 *
	 * @var int
	 */
	protected $report_length;

	/**
	 * Collection of slow tests.
	 *
	 * @var array
	 */
	protected $slow = array();

	/**
	 * Construct a new instance.
	 *
	 * @param array $options
	 */
	public function __construct( array $options = array() ) {
		$this->loadOptions( $options );
	}

	/**
	 * An error occurred.
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param Exception              $e
	 * @param float                  $time
	 */
	public function addError( PHPUnit\Framework\Test $test, Throwable $t, float $time ): void {
	}

	/**
	 * A warning occurred.
	 *
	 * @param PHPUnit_Framework_Test    $test
	 * @param PHPUnit_Framework_Warning $e
	 * @param float                     $time
	 * @since Method available since Release 5.1.0
	 */
	public function addWarning( PHPUnit\Framework\Test $test, PHPUnit\Framework\Warning $e, float $time ): void {
	}

	/**
	 * 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, float $time ): void {
	}

	/**
	 * Incomplete test.
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param Exception              $e
	 * @param float                  $time
	 */
	public function addIncompleteTest( PHPUnit\Framework\Test $test, Throwable $t, float $time ): void {
	}

	/**
	 * Risky test.
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param Exception              $e
	 * @param float                  $time
	 * @since  Method available since Release 4.0.0
	 */
	public function addRiskyTest( PHPUnit\Framework\Test $test, Throwable $t, float $time ): void {
	}

	/**
	 * Skipped test.
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param Exception              $e
	 * @param float                  $time
	 */
	public function addSkippedTest( PHPUnit\Framework\Test $test, Throwable $t, float $time ): void {
	}

	/**
	 * A test started.
	 *
	 * @param PHPUnit_Framework_Test $test
	 */
	public function startTest( PHPUnit\Framework\Test $test ): void {
	}

	/**
	 * A test ended.
	 *
	 * @param PHPUnit_Framework_Test $test
	 * @param float                  $time
	 */
	public function endTest( PHPUnit\Framework\Test $test, float $time ): void {
		if ( ! $test instanceof PHPUnit_Framework_TestCase ) {
			return;
		}

		$time      = $this->toMilliseconds( $time );
		$threshold = $this->getSlowThreshold( $test );

		if ( $this->isSlow( $time, $threshold ) ) {
			$this->addSlowTest( $test, $time );
		}
	}

	/**
	 * A test suite started.
	 *
	 * @param PHPUnit_Framework_TestSuite $suite
	 */
	public function startTestSuite( PHPUnit\Framework\TestSuite $suite ): void {
		$this->suites++;
	}

	/**
	 * A test suite ended.
	 *
	 * @param PHPUnit_Framework_TestSuite $suite
	 */
	public function endTestSuite( PHPUnit\Framework\TestSuite $suite ): void {
		$this->suites--;

		if ( 0 === $this->suites && $this->hasSlowTests() ) {
			arsort( $this->slow ); // Sort longest running tests to the top.

			$this->renderHeader();
			$this->renderBody();
			$this->renderFooter();
		}
	}

	/**
	 * Whether the given test execution time is considered slow.
	 *
	 * @param int $time           Test execution time in milliseconds
	 * @param int $slow_threshold Test execution time at which a test should be considered slow (milliseconds)
	 * @return bool
	 */
	protected function isSlow( $time, $slow_threshold ) {
		return $time >= $slow_threshold;
	}

	/**
	 * Stores a test as slow.
	 *
	 * @param PHPUnit_Framework_TestCase $test
	 * @param int                        $time Test execution time in milliseconds
	 */
	protected function addSlowTest( PHPUnit_Framework_TestCase $test, $time ) {
		$label = $this->makeLabel( $test );

		$this->slow[ $label ] = $time;
	}

	/**
	 * Whether at least one test has been considered slow.
	 *
	 * @return bool
	 */
	protected function hasSlowTests() {
		return ! empty( $this->slow );
	}

	/**
	 * Convert PHPUnit's reported test time (microseconds) to milliseconds.
	 *
	 * @param float $time
	 * @return int
	 */
	protected function toMilliseconds( $time ) {
		return (int) round( $time * 1000 );
	}

	/**
	 * Label for describing a test.
	 *
	 * @param PHPUnit_Framework_TestCase $test
	 * @return string
	 */
	protected function makeLabel( PHPUnit_Framework_TestCase $test ) {
		return sprintf( '%s:%s', get_class( $test ), $test->getName() );
	}

	/**
	 * Calculate number of slow tests to report about.
	 *
	 * @return int
	 */
	protected function getReportLength() {
		return min( count( $this->slow ), $this->report_length );
	}

	/**
	 * Find how many slow tests occurred that won't be shown due to list length.
	 *
	 * @return int Number of hidden slow tests
	 */
	protected function getHiddenCount() {
		$total   = count( $this->slow );
		$showing = $this->getReportLength( $this->slow );

		$hidden = 0;
		if ( $total > $showing ) {
			$hidden = $total - $showing;
		}

		return $hidden;
	}

	/**
	 * Renders slow test report header.
	 */
	protected function renderHeader() {
		echo sprintf( "\n\nYou should really fix these slow tests (>%sms)...\n", $this->slow_threshold );
	}

	/**
	 * Renders slow test report body.
	 */
	protected function renderBody() {
		$slow_tests = $this->slow;

		$length = $this->getReportLength( $slow_tests );
		for ( $i = 1; $i <= $length; ++$i ) {
			$label = key( $slow_tests );
			$time  = array_shift( $slow_tests );

			echo sprintf( " %s. %sms to run %s\n", $i, $time, $label );
		}
	}

	/**
	 * Renders slow test report footer.
	 */
	protected function renderFooter() {
		$hidden = $this->getHiddenCount( $this->slow );
		if ( $hidden ) {
			echo sprintf( '...and there %s %s more above your threshold hidden from view', 1 === $hidden ? 'is' : 'are', $hidden );
		}
	}

	/**
	 * Populate options into class internals.
	 *
	 * @param array $options
	 */
	protected function loadOptions( array $options ) {
		$this->slow_threshold = isset( $options['slowThreshold'] ) ? $options['slowThreshold'] : 500;
		$this->report_length  = isset( $options['reportLength'] ) ? $options['reportLength'] : 10;
	}

	/**
	 * Get slow test threshold for given test. A TestCase can override the
	 * suite-wide slow threshold by using the annotation @slowThreshold with
	 * the threshold value in milliseconds.
	 *
	 * The following test will only be considered slow when its execution time
	 * reaches 5000ms (5 seconds):
	 *
	 * <code>
	 *
	 * @slowThreshold 5000
	 * public function testLongRunningProcess() {}
	 * </code>
	 *
	 * @param PHPUnit_Framework_TestCase $test
	 * @return int
	 */
	protected function getSlowThreshold( PHPUnit_Framework_TestCase $test ) {
		$ann = $test->getAnnotations();

		return isset( $ann['method']['slowThreshold'][0] ) ? $ann['method']['slowThreshold'][0] : $this->slow_threshold;
	}
}
