From e23e5a7682381bfdea8b5f77320664d0ae51e80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 5 Dec 2025 17:47:42 +0100 Subject: [PATCH] PHPORM-432 Wait for search indexes to be dropped before creating new one in tests (#3460) * Use a distinct collection names for each test using atlas search indexes * Revert to only wait for the previous search indexes to be dropped * Factorize search indexes waiting helper --- .../fundamentals/as-avs/AtlasSearchTest.php | 19 +++---- tests/AtlasSearchIndexManagement.php | 50 +++++++++++++++++++ tests/AtlasSearchTest.php | 23 ++++----- tests/Scout/ScoutIntegrationTest.php | 21 +++++--- 4 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 tests/AtlasSearchIndexManagement.php diff --git a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php index 79dfe46df..6e056f7bb 100644 --- a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php +++ b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php @@ -8,8 +8,10 @@ use Illuminate\Support\Facades\DB; use MongoDB\Builder\Query; use MongoDB\Builder\Search; +use MongoDB\Collection; use MongoDB\Driver\Exception\ServerException; use MongoDB\Laravel\Schema\Builder; +use MongoDB\Laravel\Tests\AtlasSearchIndexManagement; use MongoDB\Laravel\Tests\TestCase; use PHPUnit\Framework\Attributes\Group; @@ -18,11 +20,12 @@ use function rand; use function range; use function srand; -use function usleep; #[Group('atlas-search')] class AtlasSearchTest extends TestCase { + use AtlasSearchIndexManagement; + private array $vectors; protected function setUp(): void @@ -32,6 +35,7 @@ protected function setUp(): void parent::setUp(); $moviesCollection = DB::connection('mongodb')->getCollection('movies'); + self::assertInstanceOf(Collection::class, $moviesCollection); $moviesCollection->drop(); Movie::insert([ @@ -49,7 +53,7 @@ protected function setUp(): void ['title' => 'D', 'plot' => 'Stranded on a distant planet, astronauts must repair their ship before supplies run out.'], ])); - $moviesCollection = DB::connection('mongodb')->getCollection('movies'); + $this->waitForSearchIndexesDropped($moviesCollection); try { $moviesCollection->createSearchIndex([ @@ -82,16 +86,7 @@ protected function setUp(): void throw $e; } - // Waits for the index to be ready - do { - $ready = true; - usleep(10_000); - foreach ($moviesCollection->listSearchIndexes() as $index) { - if ($index['status'] !== 'READY') { - $ready = false; - } - } - } while (! $ready); + $this->waitForSearchIndexesReady($moviesCollection); } /** diff --git a/tests/AtlasSearchIndexManagement.php b/tests/AtlasSearchIndexManagement.php new file mode 100644 index 000000000..acf22a422 --- /dev/null +++ b/tests/AtlasSearchIndexManagement.php @@ -0,0 +1,50 @@ +listSearchIndexes()->count()) { + if (hrtime()[0] > $timeout) { + throw new RuntimeException('Timed out waiting for search indexes to be dropped'); + } + + usleep(1000); + } + } + + /** + * Waits for all search indexes to be ready + */ + public function waitForSearchIndexesReady(Collection $collection) + { + $timeout = hrtime()[0] + 30; + do { + if (hrtime()[0] > $timeout) { + throw new RuntimeException('Timed out waiting for search indexes to be ready'); + } + + usleep(1000); + $ready = true; + foreach ($collection->listSearchIndexes() as $index) { + $ready = $ready && $index['queryable']; + } + } while (! $ready); + } +} diff --git a/tests/AtlasSearchTest.php b/tests/AtlasSearchTest.php index 43848c09a..3c9948211 100644 --- a/tests/AtlasSearchTest.php +++ b/tests/AtlasSearchTest.php @@ -19,18 +19,23 @@ use function rand; use function range; use function srand; -use function usleep; use function usort; #[Group('atlas-search')] class AtlasSearchTest extends TestCase { + use AtlasSearchIndexManagement; + private array $vectors; public function setUp(): void { parent::setUp(); + $collection = $this->getConnection('mongodb')->getCollection('books'); + assert($collection instanceof MongoDBCollection); + $collection->drop(); + Book::insert($this->addVector([ ['title' => 'Introduction to Algorithms'], ['title' => 'Clean Code: A Handbook of Agile Software Craftsmanship'], @@ -54,10 +59,9 @@ public function setUp(): void ['title' => 'Pattern Recognition and Machine Learning'], ])); - $collection = $this->getConnection('mongodb')->getCollection('books'); - assert($collection instanceof MongoDBCollection); - try { + $this->waitForSearchIndexesDropped($collection); + $collection->createSearchIndex([ 'mappings' => [ 'fields' => [ @@ -89,16 +93,7 @@ public function setUp(): void throw $e; } - // Wait for the index to be ready - do { - $ready = true; - usleep(10_000); - foreach ($collection->listSearchIndexes() as $index) { - if ($index['status'] !== 'READY') { - $ready = false; - } - } - } while (! $ready); + $this->waitForSearchIndexesReady($collection); } public function tearDown(): void diff --git a/tests/Scout/ScoutIntegrationTest.php b/tests/Scout/ScoutIntegrationTest.php index b40a455ab..f493b85fd 100644 --- a/tests/Scout/ScoutIntegrationTest.php +++ b/tests/Scout/ScoutIntegrationTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\LazyCollection; use Laravel\Scout\ScoutServiceProvider; use LogicException; +use MongoDB\Laravel\Tests\AtlasSearchIndexManagement; use MongoDB\Laravel\Tests\Scout\Models\ScoutUser; use MongoDB\Laravel\Tests\Scout\Models\SearchableInSameNamespace; use MongoDB\Laravel\Tests\TestCase; @@ -17,6 +18,7 @@ use function array_merge; use function count; use function env; +use function hrtime; use function iterator_to_array; use function Orchestra\Testbench\artisan; use function range; @@ -26,6 +28,8 @@ #[Group('atlas-search')] class ScoutIntegrationTest extends TestCase { + use AtlasSearchIndexManagement; + #[Override] protected function getPackageProviders($app): array { @@ -96,14 +100,17 @@ public function setUp(): void /** This test create the search index for tests performing search */ public function testItCanCreateTheCollection() { + $this->skipIfSearchIndexManagementIsNotSupported(); + $collection = DB::connection('mongodb')->getCollection('prefix_scout_users'); $collection->drop(); + $this->waitForSearchIndexesDropped($collection); // Recreate the indexes using the artisan commands // Ensure they return a success exit code (0) self::assertSame(0, artisan($this, 'scout:delete-index', ['name' => ScoutUser::class])); - self::assertSame(0, artisan($this, 'scout:index', ['name' => ScoutUser::class])); self::assertSame(0, artisan($this, 'scout:import', ['model' => ScoutUser::class])); + self::assertSame(0, artisan($this, 'scout:index', ['name' => ScoutUser::class])); self::assertSame(44, $collection->countDocuments()); @@ -111,8 +118,10 @@ public function testItCanCreateTheCollection() self::assertCount(1, $searchIndexes); self::assertSame(['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]], iterator_to_array($searchIndexes)[0]['latestDefinition']); + $this->waitForSearchIndexesReady($collection); + // Wait for all documents to be indexed asynchronously - $i = 100; + $timeout = hrtime()[0] + 30; while (true) { $indexedDocuments = $collection->aggregate([ ['$search' => ['index' => 'scout', 'exists' => ['path' => 'name']]], @@ -122,11 +131,11 @@ public function testItCanCreateTheCollection() break; } - if ($i-- === 0) { - self::fail('Documents not indexed'); + if (hrtime()[0] > $timeout) { + self::fail('Timed out waiting for documents to be indexed'); } - usleep(100_000); + usleep(1000); } self::assertCount(44, $indexedDocuments); @@ -135,7 +144,7 @@ public function testItCanCreateTheCollection() #[Depends('testItCanCreateTheCollection')] public function testItCanUseBasicSearch() { - // All the search queries use "sort" option to ensure the results are deterministic + // All the search queries use the "sort" option to ensure the results are deterministic $results = ScoutUser::search('lar')->take(10)->orderBy('id')->get(); self::assertSame([