From d67a451aded7c1ecb17c9e9f28108b785a41c4ec Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Mon, 1 Dec 2025 11:25:04 -0500 Subject: [PATCH 1/3] Fix search index failure on empty "Options" --- .evergreen/config.yml | 18 +++--- Taskfile.yml | 2 +- .../integration/search_index_prose_test.go | 2 +- internal/integration/search_index_test.go | 62 +++++++++++++++++++ mongo/search_index_view.go | 2 +- 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 internal/integration/search_index_test.go diff --git a/.evergreen/config.yml b/.evergreen/config.yml index f57ffb1e75..df75f2ca6a 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -26,11 +26,6 @@ timeout: args: [ls, -la] functions: - assume-test-secrets-ec2-role: - - command: ec2.assume_role - params: - role_arn: ${aws_test_secrets_role} - setup-system: # Executes clone and applies the submitted patch, if any - command: git.get_project @@ -439,7 +434,7 @@ functions: params: binary: "bash" env: - TEST_SEARCH_INDEX: "${MONGODB_URI}" + SEARCH_INDEX_URI: "${SEARCH_INDEX_URI}" args: [*task-runner, evg-test-search-index] add-aws-auth-variables-to-file: @@ -2063,7 +2058,7 @@ task_groups: params: working_dir: src/go.mongodb.org/mongo-driver binary: bash - include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN] + include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, MONGODB_URI] env: MONGODB_VERSION: ${VERSION} LAMBDA_STACK_NAME: dbx-go-lambda @@ -2072,6 +2067,15 @@ task_groups: - command: expansions.update params: file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml + - command: shell.exec + params: + working_dir: src/go.mongodb.org/mongo-driver + shell: bash + script: |- + echo "SEARCH_INDEX_URI: ${MONGODB_URI}" > atlas-expansion.yml + - command: expansions.update + params: + file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml teardown_group: - command: subprocess.exec params: diff --git a/Taskfile.yml b/Taskfile.yml index 5fd8210bda..ba92a37b4f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -160,7 +160,7 @@ tasks: evg-test-search-index: # Use the long timeout to wait for the responses from the server. - - go test ./internal/integration -run TestSearchIndexProse -v -timeout {{.LONG_TEST_TIMEOUT}}s >> test.suite + - go test ./internal/integration -run TestSearchIndex -v -timeout {{.LONG_TEST_TIMEOUT}}s >> test.suite evg-test-ocsp: - go test -v ./mongo -run TestOCSP ${OCSP_TLS_SHOULD_SUCCEED} >> test.suite diff --git a/internal/integration/search_index_prose_test.go b/internal/integration/search_index_prose_test.go index a9fbfef4a4..526e63c281 100644 --- a/internal/integration/search_index_prose_test.go +++ b/internal/integration/search_index_prose_test.go @@ -31,7 +31,7 @@ func TestSearchIndexProse(t *testing.T) { const timeout = 5 * time.Minute - uri := os.Getenv("TEST_INDEX_URI") + uri := os.Getenv("SEARCH_INDEX_URI") if uri == "" { t.Skip("skipping") } diff --git a/internal/integration/search_index_test.go b/internal/integration/search_index_test.go new file mode 100644 index 0000000000..2cb4de5eec --- /dev/null +++ b/internal/integration/search_index_test.go @@ -0,0 +1,62 @@ +// Copyright (C) MongoDB, Inc. 2025-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package integration + +import ( + "context" + "os" + "testing" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" + "go.mongodb.org/mongo-driver/v2/internal/require" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" +) + +func TestSearchIndex(t *testing.T) { + t.Parallel() + + const timeout = 5 * time.Minute + + uri := os.Getenv("SEARCH_INDEX_URI") + if uri == "" { + t.Skip("skipping") + } + + opts := options.Client().ApplyURI(uri).SetTimeout(timeout) + mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet)) + + mt.Run("search indexes with empty option", func(mt *mtest.T) { + ctx := context.Background() + + expected := bson.RawArray(bsoncore.NewArrayBuilder().AppendDocument( + bsoncore.NewDocumentBuilder(). + AppendDocument("definition", bsoncore.NewDocumentBuilder(). + AppendDocument("mappings", bsoncore.NewDocumentBuilder(). + AppendBoolean("dynamic", true). + Build()). + Build()). + Build()). + Build()) + + _, err := mt.Coll.SearchIndexes().CreateOne(ctx, mongo.SearchIndexModel{ + Definition: bson.D{ + {"mappings", bson.D{ + {"dynamic", true}, + }}, + }, + }) + require.NoError(mt, err, "failed to create index") + evt := mt.GetStartedEvent() + actual, ok := evt.Command.Lookup("indexes").ArrayOK() + require.True(mt, ok, "expected command %v to contain an indexes array", evt.Command) + require.Equal(mt, expected, actual, "expected indexes array %v, got %v", expected, actual) + }) +} diff --git a/mongo/search_index_view.go b/mongo/search_index_view.go index 5df3b8fb4e..2ac92eb8c2 100644 --- a/mongo/search_index_view.go +++ b/mongo/search_index_view.go @@ -123,13 +123,13 @@ func (siv SearchIndexView) CreateMany( } var iidx int32 + iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) if model.Options != nil { searchIndexArgs, err := mongoutil.NewOptions[options.SearchIndexesOptions](model.Options) if err != nil { return nil, fmt.Errorf("failed to construct options from builder: %w", err) } - iidx, indexes = bsoncore.AppendDocumentElementStart(indexes, strconv.Itoa(i)) if searchIndexArgs.Name != nil { indexes = bsoncore.AppendStringElement(indexes, "name", *searchIndexArgs.Name) } From a2fb993c87bb00db4f2b18373b35f9c75ed38721 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Mon, 1 Dec 2025 19:53:05 -0500 Subject: [PATCH 2/3] update prose test --- Taskfile.yml | 2 +- .../integration/search_index_prose_test.go | 59 +++++++++++++++--- internal/integration/search_index_test.go | 62 ------------------- 3 files changed, 50 insertions(+), 73 deletions(-) delete mode 100644 internal/integration/search_index_test.go diff --git a/Taskfile.yml b/Taskfile.yml index ba92a37b4f..744b92ab58 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -160,7 +160,7 @@ tasks: evg-test-search-index: # Use the long timeout to wait for the responses from the server. - - go test ./internal/integration -run TestSearchIndex -v -timeout {{.LONG_TEST_TIMEOUT}}s >> test.suite + - go test ./internal/integration -run ^TestSearchIndexProse$ -v -timeout {{.LONG_TEST_TIMEOUT}}s >> test.suite evg-test-ocsp: - go test -v ./mongo -run TestOCSP ${OCSP_TLS_SHOULD_SUCCEED} >> test.suite diff --git a/internal/integration/search_index_prose_test.go b/internal/integration/search_index_prose_test.go index 526e63c281..adbf42580d 100644 --- a/internal/integration/search_index_prose_test.go +++ b/internal/integration/search_index_prose_test.go @@ -70,7 +70,7 @@ func TestSearchIndexProse(t *testing.T) { if name == searchName && queryable { doc = cursor.Current } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -127,7 +127,7 @@ func TestSearchIndexProse(t *testing.T) { if name == *args.Name && queryable { return cursor.Current } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -186,7 +186,7 @@ func TestSearchIndexProse(t *testing.T) { if name == searchName && queryable { doc = cursor.Current } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -201,7 +201,7 @@ func TestSearchIndexProse(t *testing.T) { if !cursor.Next(ctx) { break } - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } }) @@ -237,7 +237,7 @@ func TestSearchIndexProse(t *testing.T) { if name == searchName && queryable { doc = cursor.Current } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -262,7 +262,7 @@ func TestSearchIndexProse(t *testing.T) { if name == searchName && queryable && status == "READY" && bytes.Equal(latestDefinition, expected) { doc = cursor.Current } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -315,7 +315,7 @@ func TestSearchIndexProse(t *testing.T) { if name == searchName && queryable { doc = cursor.Current } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -363,7 +363,7 @@ func TestSearchIndexProse(t *testing.T) { doc = cursor.Current assert.Equal(mt, indexType, "search") } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -391,7 +391,7 @@ func TestSearchIndexProse(t *testing.T) { doc = cursor.Current assert.Equal(mt, indexType, "search") } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -432,7 +432,7 @@ func TestSearchIndexProse(t *testing.T) { doc = cursor.Current assert.Equal(mt, indexType, "vectorSearch") } else { - t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) time.Sleep(5 * time.Second) } } @@ -472,4 +472,43 @@ func TestSearchIndexProse(t *testing.T) { }) assert.ErrorContains(mt, err, "Attribute mappings missing") }) + + mt.Run("case 9: Drivers use server default for unspecified name (`default`) and type (`search`)", func(mt *mtest.T) { + ctx := context.Background() + + view := mt.Coll.SearchIndexes() + definition := bson.D{ + {"mappings", bson.D{ + {"dynamic", true}, + }}, + } + opts := options.SearchIndexes() + + indexName, err := view.CreateOne(ctx, mongo.SearchIndexModel{ + Definition: definition, + Options: opts, + }) + require.NoError(mt, err, "failed to create index") + require.Equal(mt, "default", indexName) + + var doc bson.Raw + for doc == nil { + cursor, err := view.List(ctx, opts) + require.NoError(mt, err, "failed to list") + + if !cursor.Next(ctx) { + break + } + name := cursor.Current.Lookup("name").StringValue() + queryable := cursor.Current.Lookup("queryable").Boolean() + indexType := cursor.Current.Lookup("type").StringValue() + if name == indexName && queryable { + doc = cursor.Current + require.Equal(mt, indexType, "search") + } else { + mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) + time.Sleep(5 * time.Second) + } + } + }) } diff --git a/internal/integration/search_index_test.go b/internal/integration/search_index_test.go deleted file mode 100644 index 2cb4de5eec..0000000000 --- a/internal/integration/search_index_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (C) MongoDB, Inc. 2025-present. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - -package integration - -import ( - "context" - "os" - "testing" - "time" - - "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" - "go.mongodb.org/mongo-driver/v2/internal/require" - "go.mongodb.org/mongo-driver/v2/mongo" - "go.mongodb.org/mongo-driver/v2/mongo/options" - "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" -) - -func TestSearchIndex(t *testing.T) { - t.Parallel() - - const timeout = 5 * time.Minute - - uri := os.Getenv("SEARCH_INDEX_URI") - if uri == "" { - t.Skip("skipping") - } - - opts := options.Client().ApplyURI(uri).SetTimeout(timeout) - mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet)) - - mt.Run("search indexes with empty option", func(mt *mtest.T) { - ctx := context.Background() - - expected := bson.RawArray(bsoncore.NewArrayBuilder().AppendDocument( - bsoncore.NewDocumentBuilder(). - AppendDocument("definition", bsoncore.NewDocumentBuilder(). - AppendDocument("mappings", bsoncore.NewDocumentBuilder(). - AppendBoolean("dynamic", true). - Build()). - Build()). - Build()). - Build()) - - _, err := mt.Coll.SearchIndexes().CreateOne(ctx, mongo.SearchIndexModel{ - Definition: bson.D{ - {"mappings", bson.D{ - {"dynamic", true}, - }}, - }, - }) - require.NoError(mt, err, "failed to create index") - evt := mt.GetStartedEvent() - actual, ok := evt.Command.Lookup("indexes").ArrayOK() - require.True(mt, ok, "expected command %v to contain an indexes array", evt.Command) - require.Equal(mt, expected, actual, "expected indexes array %v, got %v", expected, actual) - }) -} From 8b7ac2b0ee2a245ca855a90878c7ef9cca40e1cd Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Tue, 2 Dec 2025 18:30:57 -0500 Subject: [PATCH 3/3] update view.List() context in prose tests --- .../integration/search_index_prose_test.go | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/internal/integration/search_index_prose_test.go b/internal/integration/search_index_prose_test.go index adbf42580d..f529284343 100644 --- a/internal/integration/search_index_prose_test.go +++ b/internal/integration/search_index_prose_test.go @@ -57,12 +57,15 @@ func TestSearchIndexProse(t *testing.T) { require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") + awaitCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(awaitCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(awaitCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -110,7 +113,7 @@ func TestSearchIndexProse(t *testing.T) { require.Contains(mt, indexes, *args.Name) } - getDocument := func(opts *options.SearchIndexesOptionsBuilder) bson.Raw { + getDocument := func(ctx context.Context, opts *options.SearchIndexesOptionsBuilder) bson.Raw { for { cursor, err := view.List(ctx, opts) require.NoError(mt, err, "failed to list") @@ -138,7 +141,10 @@ func TestSearchIndexProse(t *testing.T) { go func(opts *options.SearchIndexesOptionsBuilder) { defer wg.Done() - doc := getDocument(opts) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + doc := getDocument(ctx, opts) require.NotNil(mt, doc, "got empty document") args, err := mongoutil.NewOptions[options.SearchIndexesOptions](opts) @@ -173,12 +179,14 @@ func TestSearchIndexProse(t *testing.T) { require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") + createOneCtx, createOneCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer createOneCancel() var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(createOneCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(createOneCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -194,11 +202,13 @@ func TestSearchIndexProse(t *testing.T) { err = view.DropOne(ctx, searchName) require.NoError(mt, err, "failed to drop index") + dropOneCtx, dropOneCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer dropOneCancel() for { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(dropOneCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(dropOneCtx) { break } mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String()) @@ -224,12 +234,14 @@ func TestSearchIndexProse(t *testing.T) { require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") + createOneCtx, createOneCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer createOneCancel() var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(createOneCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(createOneCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -248,11 +260,13 @@ func TestSearchIndexProse(t *testing.T) { require.NoError(mt, err, "failed to marshal definition") err = view.UpdateOne(ctx, searchName, definition) require.NoError(mt, err, "failed to update index") + updateOneCtx, updateOneCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer updateOneCancel() for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(updateOneCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(updateOneCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -302,12 +316,14 @@ func TestSearchIndexProse(t *testing.T) { }) require.NoError(mt, err, "failed to create index") require.Equal(mt, searchName, index, "unmatched name") + awaitCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(awaitCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(awaitCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -348,12 +364,14 @@ func TestSearchIndexProse(t *testing.T) { }) require.NoError(mt, err, "failed to create index") require.Equal(mt, indexName, index, "unmatched name") + implicitCtx, implicitCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer implicitCancel() var doc bson.Raw for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(implicitCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(implicitCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -376,12 +394,14 @@ func TestSearchIndexProse(t *testing.T) { }) require.NoError(mt, err, "failed to create index") require.Equal(mt, indexName, index, "unmatched name") + explicitCtx, explicitCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer explicitCancel() doc = nil for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(explicitCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(explicitCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -417,12 +437,14 @@ func TestSearchIndexProse(t *testing.T) { }) require.NoError(mt, err, "failed to create index") require.Equal(mt, indexName, index, "unmatched name") + vectorCtx, vectorCancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer vectorCancel() doc = nil for doc == nil { - cursor, err := view.List(ctx, opts) + cursor, err := view.List(vectorCtx, opts) require.NoError(mt, err, "failed to list") - if !cursor.Next(ctx) { + if !cursor.Next(vectorCtx) { break } name := cursor.Current.Lookup("name").StringValue() @@ -474,8 +496,6 @@ func TestSearchIndexProse(t *testing.T) { }) mt.Run("case 9: Drivers use server default for unspecified name (`default`) and type (`search`)", func(mt *mtest.T) { - ctx := context.Background() - view := mt.Coll.SearchIndexes() definition := bson.D{ {"mappings", bson.D{ @@ -484,13 +504,16 @@ func TestSearchIndexProse(t *testing.T) { } opts := options.SearchIndexes() - indexName, err := view.CreateOne(ctx, mongo.SearchIndexModel{ + indexName, err := view.CreateOne(context.Background(), mongo.SearchIndexModel{ Definition: definition, Options: opts, }) require.NoError(mt, err, "failed to create index") require.Equal(mt, "default", indexName) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + var doc bson.Raw for doc == nil { cursor, err := view.List(ctx, opts)