diff --git a/phpunit.xsd b/phpunit.xsd
index 231096f622a..dafa061f130 100644
--- a/phpunit.xsd
+++ b/phpunit.xsd
@@ -132,13 +132,16 @@
+
+
+
diff --git a/schema/13.0.xsd b/schema/13.0.xsd
index ad6ac34e3aa..f0ece607d65 100644
--- a/schema/13.0.xsd
+++ b/schema/13.0.xsd
@@ -123,13 +123,16 @@
+
+
+
diff --git a/src/Runner/TestSuiteSorter.php b/src/Runner/TestSuiteSorter.php
index c82a5aedc9f..ebad5891132 100644
--- a/src/Runner/TestSuiteSorter.php
+++ b/src/Runner/TestSuiteSorter.php
@@ -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;
@@ -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
@@ -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
@@ -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,
];
@@ -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) {
@@ -205,6 +211,21 @@ private function sortByDuration(array $tests): array
return $tests;
}
+ /**
+ * @param list $tests
+ *
+ * @return list
+ */
+ private function sortByNewest(array $tests): array
+ {
+ usort(
+ $tests,
+ fn (Test $left, Test $right) => $this->cmpNewest($left, $right),
+ );
+
+ return $tests;
+ }
+
/**
* @param list $tests
*
@@ -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.
*/
@@ -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.
diff --git a/src/TextUI/Configuration/Cli/Builder.php b/src/TextUI/Configuration/Cli/Builder.php
index 068a34e39ed..e394c6b28fb 100644
--- a/src/TextUI/Configuration/Cli/Builder.php
+++ b/src/TextUI/Configuration/Cli/Builder.php
@@ -665,6 +665,11 @@ public function fromParameters(array $parameters): Configuration
break;
+ case 'newest':
+ $executionOrder = TestSuiteSorter::ORDER_NEWEST;
+
+ break;
+
case 'no-depends':
$resolveDependencies = false;
diff --git a/src/TextUI/Configuration/Xml/Loader.php b/src/TextUI/Configuration/Xml/Loader.php
index 1cd8ded5aa7..779957f2be3 100644
--- a/src/TextUI/Configuration/Xml/Loader.php
+++ b/src/TextUI/Configuration/Xml/Loader.php
@@ -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;
diff --git a/src/TextUI/Help.php b/src/TextUI/Help.php
index 576855c6847..f3c5efcfef4 100644
--- a/src/TextUI/Help.php
+++ b/src/TextUI/Help.php
@@ -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 ', 'desc' => 'Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size'],
+ ['arg' => '--order-by ', 'desc' => 'Run tests in order: default|defects|depends|duration|newest|no-depends|random|reverse|size'],
['arg' => '--random-order-seed ', 'desc' => 'Use the specified random seed when running tests in random order'],
],
diff --git a/tests/end-to-end/_files/output-cli-help-color.txt b/tests/end-to-end/_files/output-cli-help-color.txt
index 2b4c03d1893..bcebe29d7c4 100644
--- a/tests/end-to-end/_files/output-cli-help-color.txt
+++ b/tests/end-to-end/_files/output-cli-help-color.txt
@@ -134,7 +134,7 @@
[32m--do-not-cache-result [0m Do not write test results to cache file
[32m--order-by [36m[0m [0m Run tests in order:
- default|defects|depends|duration|no-depends|random|reverse|size
+ default|defects|depends|duration|newest|no-depends|random|reverse|size
[32m--random-order-seed [36m[0m [0m Use the specified random seed when
running tests in random order
diff --git a/tests/end-to-end/_files/output-cli-usage.txt b/tests/end-to-end/_files/output-cli-usage.txt
index 90409184163..86fcd2cf877 100644
--- a/tests/end-to-end/_files/output-cli-usage.txt
+++ b/tests/end-to-end/_files/output-cli-usage.txt
@@ -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 Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size
+ --order-by Run tests in order: default|defects|depends|duration|newest|no-depends|random|reverse|size
--random-order-seed Use the specified random seed when running tests in random order
Reporting:
diff --git a/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/MiddleTest.php b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/MiddleTest.php
new file mode 100644
index 00000000000..68380969de6
--- /dev/null
+++ b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/MiddleTest.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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);
+ }
+}
diff --git a/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/NewTest.php b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/NewTest.php
new file mode 100644
index 00000000000..e1a0d869a52
--- /dev/null
+++ b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/NewTest.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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);
+ }
+}
diff --git a/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/OldTest.php b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/OldTest.php
new file mode 100644
index 00000000000..7315dc2aeff
--- /dev/null
+++ b/tests/end-to-end/execution-order/fixture/test-classes-with-different-modification-times/OldTest.php
@@ -0,0 +1,22 @@
+
+ *
+ * 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);
+ }
+}
diff --git a/tests/end-to-end/execution-order/order-by-newest-test-classes.phpt b/tests/end-to-end/execution-order/order-by-newest-test-classes.phpt
new file mode 100644
index 00000000000..5dd09e0f373
--- /dev/null
+++ b/tests/end-to-end/execution-order/order-by-newest-test-classes.phpt
@@ -0,0 +1,52 @@
+--TEST--
+Order by newest: Test classes with different modification times
+--FILE--
+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)
diff --git a/tests/end-to-end/execution-order/order-by-newest-xml-test-classes.phpt b/tests/end-to-end/execution-order/order-by-newest-xml-test-classes.phpt
new file mode 100644
index 00000000000..589cd711b22
--- /dev/null
+++ b/tests/end-to-end/execution-order/order-by-newest-xml-test-classes.phpt
@@ -0,0 +1,66 @@
+--TEST--
+Order by newest: Test classes with different modification times
+--FILE--
+
+
+
+
+ {$fixture_dir}
+
+
+
+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)
diff --git a/tests/unit/TextUI/Configuration/Cli/BuilderTest.php b/tests/unit/TextUI/Configuration/Cli/BuilderTest.php
index df35dee41cb..106235830a1 100644
--- a/tests/unit/TextUI/Configuration/Cli/BuilderTest.php
+++ b/tests/unit/TextUI/Configuration/Cli/BuilderTest.php
@@ -1126,6 +1126,17 @@ public function testOrderByDuration(): void
$this->assertFalse($configuration->hasResolveDependencies());
}
+ #[TestDox('--order-by newest')]
+ public function testOrderByNewest(): void
+ {
+ $configuration = (new Builder)->fromParameters(['--order-by', 'newest']);
+
+ $this->assertTrue($configuration->hasExecutionOrder());
+ $this->assertSame(TestSuiteSorter::ORDER_NEWEST, $configuration->executionOrder());
+ $this->assertFalse($configuration->hasExecutionOrderDefects());
+ $this->assertFalse($configuration->hasResolveDependencies());
+ }
+
#[TestDox('--order-by random')]
public function testOrderByRandom(): void
{
diff --git a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
index ff3902155cc..a56e6c1891e 100644
--- a/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
+++ b/tests/unit/TextUI/Configuration/Xml/LoaderTest.php
@@ -39,6 +39,7 @@ public static function configurationRootOptionsProvider(): array
{
return [
'executionOrder default' => ['executionOrder', 'default', TestSuiteSorter::ORDER_DEFAULT],
+ 'executionOrder newest' => ['executionOrder', 'newest', TestSuiteSorter::ORDER_NEWEST],
'executionOrder random' => ['executionOrder', 'random', TestSuiteSorter::ORDER_RANDOMIZED],
'executionOrder reverse' => ['executionOrder', 'reverse', TestSuiteSorter::ORDER_REVERSED],
'executionOrder size' => ['executionOrder', 'size', TestSuiteSorter::ORDER_SIZE],