From f4c2e44110bab46bc24ff73f9ca2898eef956c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 4 Dec 2025 20:31:48 +0100 Subject: [PATCH 1/3] Use a distinct collection names for each test using atlas search indexes --- .../fundamentals/as-avs/AtlasSearchTest.php | 11 ++++--- tests/AtlasSearchTest.php | 32 +++++++++++++------ tests/Models/Book.php | 9 ++++++ tests/Scout/ScoutIntegrationTest.php | 7 ++-- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php index 79dfe46df..918bfe8a0 100644 --- a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php +++ b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php @@ -8,6 +8,7 @@ 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\TestCase; @@ -32,6 +33,7 @@ protected function setUp(): void parent::setUp(); $moviesCollection = DB::connection('mongodb')->getCollection('movies'); + self::assertInstanceOf(Collection::class, $moviesCollection); $moviesCollection->drop(); Movie::insert([ @@ -49,7 +51,10 @@ 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'); + // Waits for the search index created in the previous test to be deleted + do { + usleep(1_000); + } while ($moviesCollection->listSearchIndexes()->count()); try { $moviesCollection->createSearchIndex([ @@ -87,9 +92,7 @@ protected function setUp(): void $ready = true; usleep(10_000); foreach ($moviesCollection->listSearchIndexes() as $index) { - if ($index['status'] !== 'READY') { - $ready = false; - } + $ready = $ready && $index['queryable']; } } while (! $ready); } diff --git a/tests/AtlasSearchTest.php b/tests/AtlasSearchTest.php index 43848c09a..502ea685a 100644 --- a/tests/AtlasSearchTest.php +++ b/tests/AtlasSearchTest.php @@ -29,6 +29,10 @@ class AtlasSearchTest extends TestCase public function setUp(): void { + // Use a unique prefix per test to avoid collisions when the search index is + // deleted asynchronously while we try to create it in setup of the next test + $_SERVER['DB_PREFIX'] = 'AtlasSearchTest_' . $this->name() . '_'; + parent::setUp(); Book::insert($this->addVector([ @@ -54,7 +58,7 @@ public function setUp(): void ['title' => 'Pattern Recognition and Machine Learning'], ])); - $collection = $this->getConnection('mongodb')->getCollection('books'); + $collection = $this->getConnection('mongodb')->getCollection($this->getBookCollectionName()); assert($collection instanceof MongoDBCollection); try { @@ -92,25 +96,24 @@ public function setUp(): void // Wait for the index to be ready do { $ready = true; - usleep(10_000); + usleep(100); foreach ($collection->listSearchIndexes() as $index) { - if ($index['status'] !== 'READY') { - $ready = false; - } + $ready = $ready && $index['queryable']; } } while (! $ready); } public function tearDown(): void { - $this->getConnection('mongodb')->getCollection('books')->drop(); + $this->getConnection('mongodb')->getCollection($this->getBookCollectionName())->drop(); + unset($_SERVER['DB_PREFIX']); parent::tearDown(); } public function testGetIndexes() { - $indexes = Schema::getIndexes('books'); + $indexes = Schema::getIndexes($this->getBookCollectionName()); self::assertIsArray($indexes); self::assertCount(4, $indexes); @@ -171,7 +174,7 @@ public function testEloquentBuilderSearch() public function testDatabaseBuilderSearch() { - $results = $this->getConnection('mongodb')->table('books') + $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) ->search(Search::text('title', 'systems'), sort: ['title' => 1]); self::assertInstanceOf(LaravelCollection::class, $results); @@ -199,7 +202,7 @@ public function testEloquentBuilderAutocomplete() public function testDatabaseBuilderAutocomplete() { - $results = $this->getConnection('mongodb')->table('books') + $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) ->autocomplete('title', 'system'); self::assertInstanceOf(LaravelCollection::class, $results); @@ -213,7 +216,7 @@ public function testDatabaseBuilderAutocomplete() public function testDatabaseBuilderVectorSearch() { - $results = $this->getConnection('mongodb')->table('books') + $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) ->vectorSearch( index: 'vector', path: 'vector4', @@ -253,6 +256,15 @@ public function testEloquentBuilderVectorSearch() ); } + private function getBookCollectionName(): string + { + $name = (new Book())->getTable(); + + self::assertStringStartsWith('AtlasSearchTest_', $name); + + return $name; + } + /** Generate random vectors using fixed seed to make tests deterministic */ private function addVector(array $items): array { diff --git a/tests/Models/Book.php b/tests/Models/Book.php index 3293a0eaa..93c6f52bb 100644 --- a/tests/Models/Book.php +++ b/tests/Models/Book.php @@ -23,6 +23,15 @@ class Book extends Model protected $table = 'books'; protected static $unguarded = true; + public function __construct(array $attributes = []) + { + /* @TODO remove when connection prefix is supported + * @see https://jira.mongodb.org/browse/PHPORM-433 */ + $this->table = ($_SERVER['DB_PREFIX'] ?? '') . $this->table; + + parent::__construct($attributes); + } + public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); diff --git a/tests/Scout/ScoutIntegrationTest.php b/tests/Scout/ScoutIntegrationTest.php index b40a455ab..72de7b83d 100644 --- a/tests/Scout/ScoutIntegrationTest.php +++ b/tests/Scout/ScoutIntegrationTest.php @@ -112,8 +112,9 @@ public function testItCanCreateTheCollection() self::assertSame(['mappings' => ['dynamic' => true, 'fields' => ['bool_field' => ['type' => 'boolean']]]], iterator_to_array($searchIndexes)[0]['latestDefinition']); // Wait for all documents to be indexed asynchronously - $i = 100; + $i = 1000; while (true) { + usleep(10_000); $indexedDocuments = $collection->aggregate([ ['$search' => ['index' => 'scout', 'exists' => ['path' => 'name']]], ])->toArray(); @@ -125,8 +126,6 @@ public function testItCanCreateTheCollection() if ($i-- === 0) { self::fail('Documents not indexed'); } - - usleep(100_000); } self::assertCount(44, $indexedDocuments); @@ -135,7 +134,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([ From 8e114b5dfa3ec78cea014804a0b5b1e4bade9937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 5 Dec 2025 12:22:32 +0100 Subject: [PATCH 2/3] Revert to only wait for the previous search indexes to be dropped --- .../fundamentals/as-avs/AtlasSearchTest.php | 6 +-- tests/AtlasSearchTest.php | 38 ++++++++----------- tests/Models/Book.php | 9 ----- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php index 918bfe8a0..c6f3a647f 100644 --- a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php +++ b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php @@ -52,9 +52,9 @@ protected function setUp(): void ])); // Waits for the search index created in the previous test to be deleted - do { - usleep(1_000); - } while ($moviesCollection->listSearchIndexes()->count()); + while ($moviesCollection->listSearchIndexes()->count()) { + usleep(1000); + } try { $moviesCollection->createSearchIndex([ diff --git a/tests/AtlasSearchTest.php b/tests/AtlasSearchTest.php index 502ea685a..93a1745cf 100644 --- a/tests/AtlasSearchTest.php +++ b/tests/AtlasSearchTest.php @@ -29,12 +29,12 @@ class AtlasSearchTest extends TestCase public function setUp(): void { - // Use a unique prefix per test to avoid collisions when the search index is - // deleted asynchronously while we try to create it in setup of the next test - $_SERVER['DB_PREFIX'] = 'AtlasSearchTest_' . $this->name() . '_'; - 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'], @@ -58,10 +58,12 @@ public function setUp(): void ['title' => 'Pattern Recognition and Machine Learning'], ])); - $collection = $this->getConnection('mongodb')->getCollection($this->getBookCollectionName()); - assert($collection instanceof MongoDBCollection); - try { + // Waits for the search index created in the previous test to be deleted + while ($collection->listSearchIndexes()->count()) { + usleep(1000); + } + $collection->createSearchIndex([ 'mappings' => [ 'fields' => [ @@ -96,7 +98,7 @@ public function setUp(): void // Wait for the index to be ready do { $ready = true; - usleep(100); + usleep(1000); foreach ($collection->listSearchIndexes() as $index) { $ready = $ready && $index['queryable']; } @@ -105,15 +107,14 @@ public function setUp(): void public function tearDown(): void { - $this->getConnection('mongodb')->getCollection($this->getBookCollectionName())->drop(); - unset($_SERVER['DB_PREFIX']); + $this->getConnection('mongodb')->getCollection('books')->drop(); parent::tearDown(); } public function testGetIndexes() { - $indexes = Schema::getIndexes($this->getBookCollectionName()); + $indexes = Schema::getIndexes('books'); self::assertIsArray($indexes); self::assertCount(4, $indexes); @@ -174,7 +175,7 @@ public function testEloquentBuilderSearch() public function testDatabaseBuilderSearch() { - $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) + $results = $this->getConnection('mongodb')->table('books') ->search(Search::text('title', 'systems'), sort: ['title' => 1]); self::assertInstanceOf(LaravelCollection::class, $results); @@ -202,7 +203,7 @@ public function testEloquentBuilderAutocomplete() public function testDatabaseBuilderAutocomplete() { - $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) + $results = $this->getConnection('mongodb')->table('books') ->autocomplete('title', 'system'); self::assertInstanceOf(LaravelCollection::class, $results); @@ -216,7 +217,7 @@ public function testDatabaseBuilderAutocomplete() public function testDatabaseBuilderVectorSearch() { - $results = $this->getConnection('mongodb')->table($this->getBookCollectionName()) + $results = $this->getConnection('mongodb')->table('books') ->vectorSearch( index: 'vector', path: 'vector4', @@ -256,15 +257,6 @@ public function testEloquentBuilderVectorSearch() ); } - private function getBookCollectionName(): string - { - $name = (new Book())->getTable(); - - self::assertStringStartsWith('AtlasSearchTest_', $name); - - return $name; - } - /** Generate random vectors using fixed seed to make tests deterministic */ private function addVector(array $items): array { diff --git a/tests/Models/Book.php b/tests/Models/Book.php index 93c6f52bb..3293a0eaa 100644 --- a/tests/Models/Book.php +++ b/tests/Models/Book.php @@ -23,15 +23,6 @@ class Book extends Model protected $table = 'books'; protected static $unguarded = true; - public function __construct(array $attributes = []) - { - /* @TODO remove when connection prefix is supported - * @see https://jira.mongodb.org/browse/PHPORM-433 */ - $this->table = ($_SERVER['DB_PREFIX'] ?? '') . $this->table; - - parent::__construct($attributes); - } - public function author(): BelongsTo { return $this->belongsTo(User::class, 'author_id'); From ead90dbd3626039ec9f6cf6708c6dac384fb54f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 5 Dec 2025 12:36:31 +0100 Subject: [PATCH 3/3] Factorize search indexes waiting helper --- .../fundamentals/as-avs/AtlasSearchTest.php | 18 ++----- tests/AtlasSearchIndexManagement.php | 50 +++++++++++++++++++ tests/AtlasSearchTest.php | 17 ++----- tests/Scout/ScoutIntegrationTest.php | 20 ++++++-- 4 files changed, 74 insertions(+), 31 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 c6f3a647f..6e056f7bb 100644 --- a/docs/includes/fundamentals/as-avs/AtlasSearchTest.php +++ b/docs/includes/fundamentals/as-avs/AtlasSearchTest.php @@ -11,6 +11,7 @@ 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; @@ -19,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 @@ -51,10 +53,7 @@ protected function setUp(): void ['title' => 'D', 'plot' => 'Stranded on a distant planet, astronauts must repair their ship before supplies run out.'], ])); - // Waits for the search index created in the previous test to be deleted - while ($moviesCollection->listSearchIndexes()->count()) { - usleep(1000); - } + $this->waitForSearchIndexesDropped($moviesCollection); try { $moviesCollection->createSearchIndex([ @@ -87,14 +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) { - $ready = $ready && $index['queryable']; - } - } 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 93a1745cf..3c9948211 100644 --- a/tests/AtlasSearchTest.php +++ b/tests/AtlasSearchTest.php @@ -19,12 +19,13 @@ 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 @@ -59,10 +60,7 @@ public function setUp(): void ])); try { - // Waits for the search index created in the previous test to be deleted - while ($collection->listSearchIndexes()->count()) { - usleep(1000); - } + $this->waitForSearchIndexesDropped($collection); $collection->createSearchIndex([ 'mappings' => [ @@ -95,14 +93,7 @@ public function setUp(): void throw $e; } - // Wait for the index to be ready - do { - $ready = true; - usleep(1000); - foreach ($collection->listSearchIndexes() as $index) { - $ready = $ready && $index['queryable']; - } - } while (! $ready); + $this->waitForSearchIndexesReady($collection); } public function tearDown(): void diff --git a/tests/Scout/ScoutIntegrationTest.php b/tests/Scout/ScoutIntegrationTest.php index 72de7b83d..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,10 +118,11 @@ 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 = 1000; + $timeout = hrtime()[0] + 30; while (true) { - usleep(10_000); $indexedDocuments = $collection->aggregate([ ['$search' => ['index' => 'scout', 'exists' => ['path' => 'name']]], ])->toArray(); @@ -123,9 +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(1000); } self::assertCount(44, $indexedDocuments);