Skip to content

Commit b70c885

Browse files
authored
test(NODE-6318): utf runner withTransaction callback propagates errors from operations (#4193)
1 parent 5565d50 commit b70c885

File tree

2 files changed

+90
-17
lines changed

2 files changed

+90
-17
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { type FailPoint, TestBuilder, UnifiedTestSuiteBuilder } from '../../tools/utils';
2+
3+
describe('Unified Test Runner', () => {
4+
UnifiedTestSuiteBuilder.describe('withTransaction error propagation')
5+
.runOnRequirement({ topologies: ['replicaset'], minServerVersion: '4.4.0' })
6+
.createEntities([
7+
{
8+
client: {
9+
id: 'client',
10+
useMultipleMongoses: true,
11+
uriOptions: { appName: 'bob' },
12+
observeEvents: ['commandStartedEvent', 'commandSucceededEvent', 'commandFailedEvent']
13+
}
14+
},
15+
{ database: { id: 'database', client: 'client', databaseName: 'test' } },
16+
{ collection: { id: 'collection', database: 'database', collectionName: 'coll' } },
17+
{ session: { id: 'session', client: 'client' } },
18+
19+
{ client: { id: 'failPointClient', useMultipleMongoses: false } }
20+
])
21+
.test(
22+
TestBuilder.it('should propagate the error to the withTransaction API')
23+
.operation({
24+
name: 'failPoint',
25+
object: 'testRunner',
26+
arguments: {
27+
client: 'failPointClient',
28+
failPoint: {
29+
configureFailPoint: 'failCommand',
30+
mode: { times: 1 },
31+
data: { failCommands: ['insert'], errorCode: 50, appName: 'bob' }
32+
} as FailPoint
33+
}
34+
})
35+
.operation({
36+
name: 'withTransaction',
37+
object: 'session',
38+
arguments: {
39+
callback: [
40+
{
41+
name: 'insertOne',
42+
object: 'collection',
43+
arguments: { session: 'session', document: { _id: 1 } },
44+
expectError: { isClientError: false }
45+
}
46+
]
47+
},
48+
expectError: { isClientError: false }
49+
})
50+
.expectEvents({
51+
client: 'client',
52+
events: [
53+
{
54+
commandStartedEvent: {
55+
commandName: 'insert',
56+
databaseName: 'test',
57+
command: { insert: 'coll' }
58+
}
59+
},
60+
{ commandFailedEvent: { commandName: 'insert' } },
61+
{
62+
commandStartedEvent: {
63+
commandName: 'abortTransaction',
64+
databaseName: 'admin',
65+
command: { abortTransaction: 1 }
66+
}
67+
},
68+
{ commandFailedEvent: { commandName: 'abortTransaction' } }
69+
]
70+
})
71+
.toJSON()
72+
)
73+
.run();
74+
});

test/tools/unified-spec-runner/operations.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-unused-vars */
21
/* eslint-disable @typescript-eslint/no-non-null-assertion */
32

43
import { Readable } from 'node:stream';
@@ -7,16 +6,13 @@ import { pipeline } from 'node:stream/promises';
76
import { AssertionError, expect } from 'chai';
87

98
import {
10-
AbstractCursor,
119
type ChangeStream,
1210
Collection,
1311
CommandStartedEvent,
1412
Db,
1513
type Document,
16-
type GridFSFile,
1714
type MongoClient,
1815
MongoError,
19-
type ObjectId,
2016
ReadConcern,
2117
ReadPreference,
2218
SERVER_DESCRIPTION_CHANGED,
@@ -25,7 +21,7 @@ import {
2521
type TopologyType,
2622
WriteConcern
2723
} from '../../mongodb';
28-
import { getSymbolFrom, sleep } from '../../tools/utils';
24+
import { sleep } from '../../tools/utils';
2925
import { type TestConfiguration } from '../runner/config';
3026
import { EntitiesMap } from './entities';
3127
import { expectErrorCheck, resultCheck } from './match';
@@ -153,27 +149,27 @@ operations.set('assertSameLsidOnLastTwoCommands', async ({ entities, operation }
153149
expect(last.command.lsid.id.buffer.equals(secondLast.command.lsid.id.buffer)).to.be.true;
154150
});
155151

156-
operations.set('assertSessionDirty', async ({ entities, operation }) => {
152+
operations.set('assertSessionDirty', async ({ operation }) => {
157153
const session = operation.arguments!.session;
158154
expect(session.serverSession.isDirty).to.be.true;
159155
});
160156

161-
operations.set('assertSessionNotDirty', async ({ entities, operation }) => {
157+
operations.set('assertSessionNotDirty', async ({ operation }) => {
162158
const session = operation.arguments!.session;
163159
expect(session.serverSession.isDirty).to.be.false;
164160
});
165161

166-
operations.set('assertSessionPinned', async ({ entities, operation }) => {
162+
operations.set('assertSessionPinned', async ({ operation }) => {
167163
const session = operation.arguments!.session;
168164
expect(session.isPinned, 'session should be pinned').to.be.true;
169165
});
170166

171-
operations.set('assertSessionUnpinned', async ({ entities, operation }) => {
167+
operations.set('assertSessionUnpinned', async ({ operation }) => {
172168
const session = operation.arguments!.session;
173169
expect(session.isPinned, 'session should be unpinned').to.be.false;
174170
});
175171

176-
operations.set('assertSessionTransactionState', async ({ entities, operation }) => {
172+
operations.set('assertSessionTransactionState', async ({ operation }) => {
177173
const session = operation.arguments!.session;
178174

179175
const transactionStateTranslation = {
@@ -240,7 +236,7 @@ operations.set('commitTransaction', async ({ entities, operation }) => {
240236

241237
operations.set('createChangeStream', async ({ entities, operation }) => {
242238
const watchable = entities.get(operation.object);
243-
if (watchable == null || !('watch' in watchable)) {
239+
if (watchable == null || typeof watchable !== 'object' || !('watch' in watchable)) {
244240
throw new AssertionError(`Entity ${operation.object} must be watchable`);
245241
}
246242

@@ -292,7 +288,7 @@ operations.set('dropCollection', async ({ entities, operation }) => {
292288

293289
// TODO(NODE-4243): dropCollection should suppress namespace not found errors
294290
try {
295-
return await db.dropCollection(collection, opts);
291+
await db.dropCollection(collection, opts);
296292
} catch (err) {
297293
if (!/ns not found/.test(err.message)) {
298294
throw err;
@@ -544,10 +540,10 @@ operations.set('upload', async ({ entities, operation }) => {
544540
const bucket = entities.getEntity('bucket', operation.object);
545541
const { filename, source, ...options } = operation.arguments ?? {};
546542

547-
const stream = bucket.openUploadStream(operation.arguments!.filename, options);
548-
const filestream = Readable.from(Buffer.from(operation.arguments!.source.$$hexBytes, 'hex'));
543+
const stream = bucket.openUploadStream(filename, options);
544+
const fileStream = Readable.from(Buffer.from(source.$$hexBytes, 'hex'));
549545

550-
await pipeline(filestream, stream);
546+
await pipeline(fileStream, stream);
551547
return stream.gridFSFile?._id;
552548
});
553549

@@ -720,7 +716,7 @@ operations.set('withTransaction', async ({ entities, operation, client, testConf
720716

721717
await session.withTransaction(async () => {
722718
for (const callbackOperation of operation.arguments!.callback) {
723-
await executeOperationAndCheck(callbackOperation, entities, client, testConfig);
719+
await executeOperationAndCheck(callbackOperation, entities, client, testConfig, true);
724720
}
725721
}, options);
726722
});
@@ -935,7 +931,8 @@ export async function executeOperationAndCheck(
935931
operation: OperationDescription,
936932
entities: EntitiesMap,
937933
client: MongoClient,
938-
testConfig: TestConfiguration
934+
testConfig: TestConfiguration,
935+
rethrow = false
939936
): Promise<void> {
940937
const opFunc = operations.get(operation.name);
941938
expect(opFunc, `Unknown operation: ${operation.name}`).to.exist;
@@ -956,10 +953,12 @@ export async function executeOperationAndCheck(
956953
} catch (error) {
957954
if (operation.expectError) {
958955
expectErrorCheck(error, operation.expectError, entities);
956+
if (rethrow) throw error;
959957
return;
960958
} else if (!operation.ignoreResultAndError || error instanceof MalformedOperationError) {
961959
throw error;
962960
}
961+
if (rethrow) throw error;
963962
}
964963

965964
// We check the positive outcome here so the try-catch above doesn't catch our chai assertions

0 commit comments

Comments
 (0)