Skip to content
40 changes: 27 additions & 13 deletions internal/assert/assertions.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copied from https://github.com/stretchr/testify/blob/1333b5d3bda8cf5aedcf3e1aaa95cac28aaab892/assert/assertions.go

// Copyright 2020 Mat Ryer, Tyler Bunnell and all contributors. All rights reserved.
// Use of this source code is governed by an MIT-style license that can be found in
// the THIRD-PARTY-NOTICES file.
Expand Down Expand Up @@ -79,7 +78,6 @@ the problem actually occurred in calling code.*/
// of each stack frame leading from the current test to the assert call that
// failed.
func CallerInfo() []string {

var pc uintptr
var ok bool
var file string
Expand Down Expand Up @@ -307,7 +305,6 @@ func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
}

return true

}

// validateEqualArgs checks whether provided arguments can be safely used in the
Expand Down Expand Up @@ -372,7 +369,6 @@ func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interfa
}

return true

}

// NotNil asserts that the specified object is not nil.
Expand Down Expand Up @@ -411,7 +407,8 @@ func isNil(object interface{}) bool {
[]reflect.Kind{
reflect.Chan, reflect.Func,
reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice},
reflect.Ptr, reflect.Slice,
},
kind)

if isNilableKind && value.IsNil() {
Expand Down Expand Up @@ -477,7 +474,6 @@ func True(t TestingT, value bool, msgAndArgs ...interface{}) bool {
}

return true

}

// False asserts that the specified value is false.
Expand All @@ -492,7 +488,6 @@ func False(t TestingT, value bool, msgAndArgs ...interface{}) bool {
}

return true

}

// NotEqual asserts that the specified values are NOT equal.
Expand All @@ -515,7 +510,6 @@ func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{
}

return true

}

// NotEqualValues asserts that two objects are not equal even when converted to the same type
Expand All @@ -538,7 +532,6 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte
// return (true, false) if element was not found.
// return (true, true) if element was found.
func containsElement(list interface{}, element interface{}) (ok, found bool) {

listValue := reflect.ValueOf(list)
listType := reflect.TypeOf(list)
if listType == nil {
Expand Down Expand Up @@ -573,7 +566,6 @@ func containsElement(list interface{}, element interface{}) (ok, found bool) {
}
}
return true, false

}

// Contains asserts that the specified string, list(array, slice...) or map contains the
Expand All @@ -596,7 +588,6 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo
}

return true

}

// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
Expand All @@ -619,12 +610,10 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{})
}

return true

}

// isEmpty gets whether the specified object is considered empty or not.
func isEmpty(object interface{}) bool {

// get nil case out of the way
if object == nil {
return true
Expand Down Expand Up @@ -1090,3 +1079,28 @@ func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {

return pass
}

// Empty asserts that the given value is "empty".
//
// [Zero values] are "empty".
//
// Arrays are "empty" if every element is the zero value of the type (stricter than "empty").
//
// Slices, maps and channels with zero length are "empty".
//
// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty".
//
// assert.Empty(t, obj)
//
// [Zero values]: https://go.dev/ref/spec#The_zero_value
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
pass := isEmpty(object)
if !pass {
if h, ok := t.(tHelper); ok {
h.Helper()
}
Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...)
}

return pass
}
24 changes: 0 additions & 24 deletions internal/integration/mongointernal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,4 @@ func TestNewSessionWithLSID(t *testing.T) {
// doesn't panic.
t.Errorf("expected EndSession to panic")
})

mt.Run("ClientSession.SetServer panics", func(mt *mtest.T) {
mt.Parallel()

sessionID := bson.Raw(bsoncore.NewDocumentBuilder().
AppendBinary("id", 4, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}).
Build())
sess := mongo.NewSessionWithLSID(mt.Client, sessionID)

// Use a defer-recover block to catch the expected panic and assert that
// the recovered error is not nil.
defer func() {
err := recover()
assert.NotNil(mt, err, "expected ClientSession.SetServer to panic")
}()

// Expect this call to panic.
sess.ClientSession().SetServer()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't possible now.


// We expect that calling ClientSession.SetServer on a Session returned
// by NewSessionWithLSID panics. This code will only be reached if
// ClientSession.SetServer doesn't panic.
t.Errorf("expected ClientSession.SetServer to panic")
})
}
87 changes: 50 additions & 37 deletions internal/integration/mtest/mongotest.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,51 +169,64 @@ func (t *T) Run(name string, callback func(mt *T)) {
t.RunOpts(name, NewOptions(), callback)
}

// RunOpts creates a new T instance for a sub-test with the given options. If the current environment does not satisfy
// constraints specified in the options, the new sub-test will be skipped automatically. If the test is not skipped,
// the callback will be run with the new T instance. RunOpts creates a new collection with the given name which is
// available to the callback through the T.Coll variable and is dropped after the callback returns.
func (t *T) RunOpts(name string, opts *Options, callback func(mt *T)) {
t.T.Run(name, func(wrapped *testing.T) {
sub := newT(wrapped, t.baseOpts, opts)
// Setup initializes the test client and collection for this T instance. This is
// automatically called by RunOpts but can be called manually when using New()
// directly.
func (t *T) Setup() {
// add any mock responses for this test
if t.clientType == Mock && len(t.mockResponses) > 0 {
t.AddMockResponses(t.mockResponses...)
}

// add any mock responses for this test
if sub.clientType == Mock && len(sub.mockResponses) > 0 {
sub.AddMockResponses(sub.mockResponses...)
}
if t.createClient == nil || *t.createClient {
t.createTestClient()
}

if sub.createClient == nil || *sub.createClient {
sub.createTestClient()
}
// create a collection for this test
if t.Client != nil {
t.createTestCollection()
}

// create a collection for this test
if sub.Client != nil {
sub.createTestCollection()
}
// clear any events that may have happened during setup
t.ClearEvents()
}

// defer dropping all collections if the test is using a client
defer func() {
if sub.Client == nil {
return
}
// Teardown cleans up test resources and asserts that all sessions and
// connections are closed. When using New() directly, this should be called via
// defer after Setup().
func (t *T) Teardown() {
if t.Client == nil {
return
}

// store number of sessions and connections checked out here but assert that they're equal to 0 after
// cleaning up test resources to make sure resources are always cleared
sessions := sub.Client.NumberSessionsInProgress()
conns := sub.NumberConnectionsCheckedOut()
// store number of sessions and connections checked out here but assert that they're equal to 0 after
// cleaning up test resources to make sure resources are always cleared
sessions := t.Client.NumberSessionsInProgress()
conns := t.NumberConnectionsCheckedOut()

if sub.clientType != Mock {
sub.ClearFailPoints()
sub.ClearCollections()
}
if t.clientType != Mock {
t.ClearFailPoints()
t.ClearCollections()
}

_ = t.Client.Disconnect(context.Background())
assert.Equal(t, 0, sessions, "%v sessions checked out", sessions)
assert.Equal(t, 0, conns, "%v connections checked out", conns)
}

// RunOpts creates a new T instance for a sub-test with the given options. If
// the current environment does not satisfy constraints specified in the
// options, the new sub-test will be skipped automatically. If the test is not
// skipped, the callback will be run with the new T instance. RunOpts creates a
// new collection with the given name which is available to the callback through
// the T.Coll variable and is dropped after the callback returns.
func (t *T) RunOpts(name string, opts *Options, callback func(mt *T)) {
t.T.Run(name, func(wrapped *testing.T) {
sub := newT(wrapped, t.baseOpts, opts)

_ = sub.Client.Disconnect(context.Background())
assert.Equal(sub, 0, sessions, "%v sessions checked out", sessions)
assert.Equal(sub, 0, conns, "%v connections checked out", conns)
}()
sub.Setup()
t.Cleanup(sub.Teardown)

// clear any events that may have happened during setup and run the test
sub.ClearEvents()
callback(sub)
})
}
Expand Down
76 changes: 75 additions & 1 deletion internal/integration/sessions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@ func TestSessionsProse(t *testing.T) {

limitedSessMsg := "expected session count to be less than the number of operations: %v"
assert.True(mt, limitedSessionUse, limitedSessMsg, len(ops))

})

mt.ResetClient(options.Client())
Expand Down Expand Up @@ -584,6 +583,81 @@ func TestSessionsProse(t *testing.T) {
})
}

func TestSessionsProse_21_SettingSnapshotTimeWithoutSnapshot(t *testing.T) {
// 21. Having snapshotTime set and snapshot set to false is not allowed.
mtOpts := mtest.
NewOptions().
MinServerVersion("5.0").
Topologies(mtest.ReplicaSet, mtest.Sharded)

mt := mtest.New(t, mtOpts)

mt.Setup()
Comment on lines +593 to +595
Copy link
Collaborator

@matthewdale matthewdale Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider combining these two calls into a new function to make it easier to use, like mtest.Setup. We can reconcile the two entry points in GODRIVER-3656.

E.g.

func TestBlah(...) {
	mt := mtest.Setup(t, mtOpts)
	// ...
}
package mtest

func Setup(...) *T {
	mt := mtest.New(...)
	mt.setup()
	// ...
	return mt
}

Copy link
Member Author

@prestonvasquez prestonvasquez Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest deferring to GODRIVER-3721 / GODRIVER-3656, which should end up removing Setup entirely.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Consider this resolved.

mt.Cleanup(mt.Teardown)

// Start a session by calling startSession with snapshot = false and
// snapshotTime = new Timestamp(1).
sessOpts := options.Session().SetSnapshot(false).SetSnapshotTime(bson.Timestamp{T: 1})

_, err := mt.Client.StartSession(sessOpts)
require.Error(t, err)
require.Contains(t, err.Error(), "snapshotTime cannot be set when snapshot is false")
}

func TestSessionsProse_22_SnapshotTimeGetterReturnsErrorForNonSnapshotSessions(t *testing.T) {
// 22. Retrieving `snapshotTime` on a non-snapshot session raises an error
t.Skip("Skipping test for prose 22; Go driver does not have a getter that raises an error.")
}

func TestSessionsProse_23_EnsureSnapshotTimeIsImmutable(t *testing.T) {
// 23. Ensure `snapshotTime` is Read-Only

mtOpts := mtest.
NewOptions().
MinServerVersion("5.0").
Topologies(mtest.ReplicaSet, mtest.Sharded)

mt := mtest.New(t, mtOpts)

mt.Run("multiple ClientSession calls isolation", func(mt *mtest.T) {
sess, err := mt.Client.StartSession(options.Session().SetSnapshot(false))
require.NoError(mt, err)
defer sess.EndSession(context.Background())

// Verify initial state
require.Empty(mt, sess.ClientSession().SnapshotTime)

// Attempt mutation through one ClientSession() call
client1 := sess.ClientSession()
client1.SnapshotTime = bson.Timestamp{T: 1}

// Second ClientSession() call should return independent copy
require.Empty(mt, sess.ClientSession().SnapshotTime)
})

mt.Run("snapshotTime copy is immutable", func(mt *mtest.T) {
originalTS := bson.Timestamp{T: 100, I: 5}
sess, err := mt.Client.StartSession(
options.Session().SetSnapshot(true).SetSnapshotTime(originalTS),
)
require.NoError(mt, err)
defer sess.EndSession(context.Background())

// Verify initial state
cs := sess.ClientSession()
require.True(mt, cs.SnapshotTimeSet)
require.Equal(mt, originalTS, cs.SnapshotTime)

// Mutate the copy and verify it doesn't affect the session.
cs.SnapshotTime = bson.Timestamp{T: 999, I: 888}
cs.SnapshotTimeSet = false

cs2 := sess.ClientSession()
require.True(mt, cs2.SnapshotTimeSet)
require.Equal(mt, originalTS, cs2.SnapshotTime)
})
}

type sessionFunction struct {
name string
target string
Expand Down
Loading
Loading