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")
})
}
85 changes: 48 additions & 37 deletions internal/integration/mtest/mongotest.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,51 +169,62 @@ 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()
}

_ = 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)
}()
_ = 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)
}

// clear any events that may have happened during setup and run the test
sub.ClearEvents()
// 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.Setup()
defer sub.Teardown()
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.

Instead of deferring this call, use T.Cleanup so we never have to explicitly call it. Ideally call it in Setup. Also, you can unexport Teardown since it's only called internally.

E.g.

func (t *T) Setup() {
	// ...
	t.T.Cleanup(t.teardown())
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Teardown and Setup were added in this PR so that I could use mtest with one-off non-subtest tests:

func TestSessionsProse_21_SettingSnapshotTimeWithoutSnapshot(t *testing.T) {
	mt := mtest.New(t, mtOpts)

	mt.Setup()
	defer mt.Teardown()
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there any cases where you call mt.Setup() and not call mt.Teardown() after the test? If not, setting mt.Teardown() as a test cleanup task means you don't have to remember to call it, preventing a possible programming error.

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.

defer 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