Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions phpunit.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,16 @@
<xs:enumeration value="depends"/>
<xs:enumeration value="depends,defects"/>
<xs:enumeration value="depends,duration"/>
<xs:enumeration value="depends,newest"/>
<xs:enumeration value="depends,random"/>
<xs:enumeration value="depends,reverse"/>
<xs:enumeration value="depends,size"/>
<xs:enumeration value="duration"/>
<xs:enumeration value="newest"/>
<xs:enumeration value="no-depends"/>
<xs:enumeration value="no-depends,defects"/>
<xs:enumeration value="no-depends,duration"/>
<xs:enumeration value="no-depends,newest"/>
<xs:enumeration value="no-depends,random"/>
<xs:enumeration value="no-depends,reverse"/>
<xs:enumeration value="no-depends,size"/>
Expand Down
3 changes: 3 additions & 0 deletions schema/13.0.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,16 @@
<xs:enumeration value="depends"/>
<xs:enumeration value="depends,defects"/>
<xs:enumeration value="depends,duration"/>
<xs:enumeration value="depends,newest"/>
<xs:enumeration value="depends,random"/>
<xs:enumeration value="depends,reverse"/>
<xs:enumeration value="depends,size"/>
<xs:enumeration value="duration"/>
<xs:enumeration value="newest"/>
<xs:enumeration value="no-depends"/>
<xs:enumeration value="no-depends,defects"/>
<xs:enumeration value="no-depends,duration"/>
<xs:enumeration value="no-depends,newest"/>
<xs:enumeration value="no-depends,random"/>
<xs:enumeration value="no-depends,reverse"/>
<xs:enumeration value="no-depends,size"/>
Expand Down
62 changes: 62 additions & 0 deletions src/Runner/TestSuiteSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use function array_splice;
use function assert;
use function count;
use function filemtime;
use function in_array;
use function max;
use function shuffle;
Expand All @@ -27,6 +28,7 @@
use PHPUnit\Runner\ResultCache\NullResultCache;
use PHPUnit\Runner\ResultCache\ResultCache;
use PHPUnit\Runner\ResultCache\ResultCacheId;
use ReflectionClass;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
Expand All @@ -41,6 +43,7 @@ final class TestSuiteSorter
public const int ORDER_DEFECTS_FIRST = 3;
public const int ORDER_DURATION = 4;
public const int ORDER_SIZE = 5;
public const int ORDER_NEWEST = 6;

/**
* @var non-empty-array<non-empty-string, positive-int>
Expand Down Expand Up @@ -70,6 +73,7 @@ public function reorderTestsInSuite(Test $suite, int $order, bool $resolveDepend
self::ORDER_DEFAULT,
self::ORDER_REVERSED,
self::ORDER_RANDOMIZED,
self::ORDER_NEWEST,
self::ORDER_DURATION,
self::ORDER_SIZE,
];
Expand Down Expand Up @@ -118,6 +122,8 @@ private function sort(TestSuite $suite, int $order, bool $resolveDependencies, i
$suite->setTests($this->sortByDuration($suite->tests()));
} elseif ($order === self::ORDER_SIZE) {
$suite->setTests($this->sortBySize($suite->tests()));
} elseif ($order === self::ORDER_NEWEST) {
$suite->setTests($this->sortByNewest($suite->tests()));
}

if ($orderDefects === self::ORDER_DEFECTS_FIRST) {
Expand Down Expand Up @@ -205,6 +211,21 @@ private function sortByDuration(array $tests): array
return $tests;
}

/**
* @param list<Test> $tests
*
* @return list<Test>
*/
private function sortByNewest(array $tests): array
{
usort(
$tests,
fn (Test $left, Test $right) => $this->cmpNewest($left, $right),
);

return $tests;
}

/**
* @param list<Test> $tests
*
Expand Down Expand Up @@ -260,6 +281,26 @@ private function cmpDuration(Test $a, Test $b): int
return $this->cache->time(ResultCacheId::fromReorderable($a)) <=> $this->cache->time(ResultCacheId::fromReorderable($b));
}

/**
* Compares test modified for sorting by how recent the test is
* Descending order: Newest first.
*/
private function cmpNewest(Test $a, Test $b): int
{
$result = 0;
$fileA = $this->getTestFile($a);
$fileB = $this->getTestFile($b);

if ($fileA !== null && $fileB !== null) {
$mtimeA = (int) @filemtime($fileA);
$mtimeB = (int) @filemtime($fileB);

$result = $mtimeB <=> $mtimeA;
}

return $result;
}

/**
* Compares test size for sorting tests small->medium->large->unknown.
*/
Expand All @@ -275,6 +316,27 @@ private function cmpSize(Test $a, Test $b): int
return self::SIZE_SORT_WEIGHT[$sizeA] <=> self::SIZE_SORT_WEIGHT[$sizeB];
}

/**
* Helper function for retrieving the test's file.
* Returns the first file if the argument is a TestSuite.
*/
private function getTestFile(Test $test): ?string
{
$result = null;

if ($test instanceof TestCase) {
$reflection = new ReflectionClass($test);
$filename = $reflection->getFileName();
$result = $filename !== false ? $filename : null;
}

if ($test instanceof TestSuite && count($test->tests()) > 0) {
$result = $this->getTestFile($test->tests()[0]);
}

return $result;
}

/**
* Reorder Tests within a TestCase in such a way as to resolve as many dependencies as possible.
* The algorithm will leave the tests in original running order when it can.
Expand Down
5 changes: 5 additions & 0 deletions src/TextUI/Configuration/Cli/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,11 @@ public function fromParameters(array $parameters): Configuration

break;

case 'newest':
$executionOrder = TestSuiteSorter::ORDER_NEWEST;

break;

case 'no-depends':
$resolveDependencies = false;

Expand Down
5 changes: 5 additions & 0 deletions src/TextUI/Configuration/Xml/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,11 @@ private function phpunit(string $filename, DOMDocument $document, DOMXPath $xpat

break;

case 'newest':
$executionOrder = TestSuiteSorter::ORDER_NEWEST;

break;

case 'random':
$executionOrder = TestSuiteSorter::ORDER_RANDOMIZED;

Expand Down
2 changes: 1 addition & 1 deletion src/TextUI/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ private function elements(): array
['arg' => '--do-not-cache-result', 'desc' => 'Do not write test results to cache file'],
['spacer' => ''],

['arg' => '--order-by <order>', 'desc' => 'Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size'],
['arg' => '--order-by <order>', 'desc' => 'Run tests in order: default|defects|depends|duration|newest|no-depends|random|reverse|size'],
['arg' => '--random-order-seed <N>', 'desc' => 'Use the specified random seed when running tests in random order'],
],

Expand Down
2 changes: 1 addition & 1 deletion tests/end-to-end/_files/output-cli-help-color.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
--do-not-cache-result  Do not write test results to cache file

--order-by <order>  Run tests in order:
default|defects|depends|duration|no-depends|random|reverse|size
default|defects|depends|duration|newest|no-depends|random|reverse|size
--random-order-seed <N>  Use the specified random seed when
running tests in random order

Expand Down
2 changes: 1 addition & 1 deletion tests/end-to-end/_files/output-cli-usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Execution:
--cache-result Write test results to cache file
--do-not-cache-result Do not write test results to cache file

--order-by <order> Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
--order-by <order> Run tests in order: default|defects|depends|duration|newest|no-depends|random|reverse|size
--random-order-seed <N> Use the specified random seed when running tests in random order

Reporting:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture\ExecutionOrder\ModificationTime;

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;

#[CoversNothing]
final class MiddleTest extends TestCase
{
public function testMiddle(): void
{
$this->assertTrue(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture\ExecutionOrder\ModificationTime;

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;

#[CoversNothing]
final class NewTest extends TestCase
{
public function testNew(): void
{
$this->assertTrue(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture\ExecutionOrder\ModificationTime;

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;

#[CoversNothing]
final class OldTest extends TestCase
{
public function testOld(): void
{
$this->assertTrue(true);
}
}
52 changes: 52 additions & 0 deletions tests/end-to-end/execution-order/order-by-newest-test-classes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
Order by newest: Test classes with different modification times
--FILE--
<?php declare(strict_types=1);
$fixture_dir = __DIR__ . '/fixture/test-classes-with-different-modification-times';

$_SERVER['argv'][] = '--no-configuration';
$_SERVER['argv'][] = '--do-not-cache-result';
$_SERVER['argv'][] = '--order-by';
$_SERVER['argv'][] = 'newest';
$_SERVER['argv'][] = '--debug';
$_SERVER['argv'][] = $fixture_dir;

// Force the modified times to be in order
touch("$fixture_dir/OldTest.php", strtotime('2026-01-01 00:00:00'));
touch("$fixture_dir/MiddleTest.php", strtotime('2026-01-02 00:00:00'));
touch("$fixture_dir/NewTest.php", strtotime('2026-01-03 00:00:00'));

require __DIR__ . '/../../bootstrap.php';

(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);
--EXPECTF--
PHPUnit Started (PHPUnit %s using %s)
Test Runner Configured
Event Facade Sealed
Test Suite Loaded (3 tests)
Test Runner Started
Test Suite Sorted
Test Runner Execution Started (3 tests)
Test Suite Started (CLI Arguments, 3 tests)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest, 1 test)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest, 1 test)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest, 1 test)
Test Suite Finished (CLI Arguments, 3 tests)
Test Runner Execution Finished
Test Runner Finished
PHPUnit Finished (Shell Exit Code: 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
--TEST--
Order by newest: Test classes with different modification times
--FILE--
<?php declare(strict_types=1);
$fixture_dir = __DIR__ . '/fixture/test-classes-with-different-modification-times';
$bootstrap_path = __DIR__ . '/../../bootstrap.php';

$xmlConfig = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<phpunit executionOrder="newest" bootstrap="{$bootstrap_path}">
<testsuites>
<testsuite name="newest-test">
<directory>{$fixture_dir}</directory>
</testsuite>
</testsuites>
</phpunit>
XML;

// XML config must be in a file
$configFile = tempnam(sys_get_temp_dir(), 'pnt');
file_put_contents($configFile, $xmlConfig);

$_SERVER['argv'] = ['phpunit', '--configuration', $configFile, '--debug'];

// Force the modified times to be in order
touch("$fixture_dir/OldTest.php", strtotime('2026-01-01 00:00:00'));
touch("$fixture_dir/MiddleTest.php", strtotime('2026-01-02 00:00:00'));
touch("$fixture_dir/NewTest.php", strtotime('2026-01-03 00:00:00'));

require $bootstrap_path;

(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);
--EXPECTF--
PHPUnit Started (PHPUnit %s using %s)
Test Runner Configured
Bootstrap Finished (%s)
Event Facade Sealed
Test Suite Loaded (3 tests)
Test Runner Started
Test Suite Sorted
Test Runner Execution Started (3 tests)
Test Suite Started (%s, 3 tests)
Test Suite Started (newest-test, 3 tests)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest::testNew)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\NewTest, 1 test)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest::testMiddle)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\MiddleTest, 1 test)
Test Suite Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest, 1 test)
Test Preparation Started (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Prepared (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Passed (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest::testOld)
Test Suite Finished (PHPUnit\TestFixture\ExecutionOrder\ModificationTime\OldTest, 1 test)
Test Suite Finished (newest-test, 3 tests)
Test Suite Finished (%s, 3 tests)
Test Runner Execution Finished
Test Runner Finished
PHPUnit Finished (Shell Exit Code: 0)
Loading
Loading