Skip to content

Commit 07266f2

Browse files
authored
feat(cts): add tests for chunkedBatch wrappers (#3268)
1 parent 27b82d1 commit 07266f2

File tree

31 files changed

+584
-175
lines changed

31 files changed

+584
-175
lines changed

clients/algoliasearch-client-csharp/algoliasearch/Utils/SearchClientExtensions.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -445,18 +445,18 @@ public async Task<ReplaceAllObjectsResponse> ReplaceAllObjectsAsync<T>(string in
445445

446446
var copyResponse = await OperationIndexAsync(indexName,
447447
new OperationIndexParams(OperationType.Copy, tmpIndexName)
448-
{ Scope = [ScopeType.Rules, ScopeType.Settings, ScopeType.Synonyms] }, options, cancellationToken)
448+
{ Scope = [ScopeType.Settings, ScopeType.Rules, ScopeType.Synonyms] }, options, cancellationToken)
449449
.ConfigureAwait(false);
450450

451-
var batchResponse = await ChunkedBatchAsync(tmpIndexName, objects, Action.AddObject, batchSize,
451+
var batchResponse = await ChunkedBatchAsync(tmpIndexName, objects, Action.AddObject, true, batchSize,
452452
options, cancellationToken).ConfigureAwait(false);
453453

454454
await WaitForTaskAsync(tmpIndexName, copyResponse.TaskID, requestOptions: options, ct: cancellationToken)
455455
.ConfigureAwait(false);
456456

457457
copyResponse = await OperationIndexAsync(indexName,
458458
new OperationIndexParams(OperationType.Copy, tmpIndexName)
459-
{ Scope = [ScopeType.Rules, ScopeType.Settings, ScopeType.Synonyms] }, options, cancellationToken)
459+
{ Scope = [ScopeType.Settings, ScopeType.Rules, ScopeType.Synonyms] }, options, cancellationToken)
460460
.ConfigureAwait(false);
461461
await WaitForTaskAsync(tmpIndexName, copyResponse.TaskID, requestOptions: options, ct: cancellationToken)
462462
.ConfigureAwait(false);
@@ -487,9 +487,9 @@ await WaitForTaskAsync(tmpIndexName, moveResponse.TaskID, requestOptions: option
487487
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
488488
/// <typeparam name="T"></typeparam>
489489
public List<BatchResponse> ChunkedBatch<T>(string indexName, IEnumerable<T> objects, Action action = Action.AddObject,
490-
int batchSize = 1000, RequestOptions options = null, CancellationToken cancellationToken = default)
490+
bool waitForTasks = false, int batchSize = 1000, RequestOptions options = null, CancellationToken cancellationToken = default)
491491
where T : class =>
492-
AsyncHelper.RunSync(() => ChunkedBatchAsync(indexName, objects, action, batchSize, options, cancellationToken));
492+
AsyncHelper.RunSync(() => ChunkedBatchAsync(indexName, objects, action, waitForTasks, batchSize, options, cancellationToken));
493493

494494
/// <summary>
495495
/// Helper: Chunks the given `objects` list in subset of 1000 elements max in order to make it fit in `batch` requests.
@@ -502,7 +502,7 @@ public List<BatchResponse> ChunkedBatch<T>(string indexName, IEnumerable<T> obje
502502
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
503503
/// <typeparam name="T"></typeparam>
504504
public async Task<List<BatchResponse>> ChunkedBatchAsync<T>(string indexName, IEnumerable<T> objects,
505-
Action action = Action.AddObject, int batchSize = 1000, RequestOptions options = null,
505+
Action action = Action.AddObject, bool waitForTasks = false, int batchSize = 1000, RequestOptions options = null,
506506
CancellationToken cancellationToken = default) where T : class
507507
{
508508
var batchCount = (int)Math.Ceiling((double)objects.Count() / batchSize);
@@ -518,10 +518,13 @@ public async Task<List<BatchResponse>> ChunkedBatchAsync<T>(string indexName, IE
518518
responses.Add(batchResponse);
519519
}
520520

521-
foreach (var batch in responses)
521+
if (waitForTasks)
522522
{
523-
await WaitForTaskAsync(indexName, batch.TaskID, requestOptions: options, ct: cancellationToken)
524-
.ConfigureAwait(false);
523+
foreach (var batch in responses)
524+
{
525+
await WaitForTaskAsync(indexName, batch.TaskID, requestOptions: options, ct: cancellationToken)
526+
.ConfigureAwait(false);
527+
}
525528
}
526529

527530
return responses;
@@ -544,7 +547,7 @@ public async Task<List<BatchResponse>> SaveObjectsAsync<T>(string indexName, IEn
544547
RequestOptions options = null,
545548
CancellationToken cancellationToken = default) where T : class
546549
{
547-
return await ChunkedBatchAsync(indexName, objects, Action.AddObject, 1000, options, cancellationToken).ConfigureAwait(false);
550+
return await ChunkedBatchAsync(indexName, objects, Action.AddObject, false, 1000, options, cancellationToken).ConfigureAwait(false);
548551
}
549552

550553
/// <summary>
@@ -554,11 +557,11 @@ public async Task<List<BatchResponse>> SaveObjectsAsync<T>(string indexName, IEn
554557
/// <param name="objectIDs">The list of `objectIDs` to remove from the given Algolia `indexName`.</param>
555558
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
556559
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
557-
public async Task<List<BatchResponse>> DeleteObjects(string indexName, IEnumerable<String> objectIDs,
560+
public async Task<List<BatchResponse>> DeleteObjectsAsync(string indexName, IEnumerable<String> objectIDs,
558561
RequestOptions options = null,
559562
CancellationToken cancellationToken = default)
560563
{
561-
return await ChunkedBatchAsync(indexName, objectIDs.Select(id => new { objectID = id }), Action.DeleteObject, 1000, options, cancellationToken).ConfigureAwait(false);
564+
return await ChunkedBatchAsync(indexName, objectIDs.Select(id => new { objectID = id }), Action.DeleteObject, false, 1000, options, cancellationToken).ConfigureAwait(false);
562565
}
563566

564567
/// <summary>
@@ -569,11 +572,11 @@ public async Task<List<BatchResponse>> DeleteObjects(string indexName, IEnumerab
569572
/// <param name="createIfNotExists">To be provided if non-existing objects are passed, otherwise, the call will fail.</param>
570573
/// <param name="options">Add extra http header or query parameters to Algolia.</param>
571574
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
572-
public async Task<List<BatchResponse>> PartialUpdateObjects<T>(string indexName, IEnumerable<T> objects, bool createIfNotExists,
575+
public async Task<List<BatchResponse>> PartialUpdateObjectsAsync<T>(string indexName, IEnumerable<T> objects, bool createIfNotExists,
573576
RequestOptions options = null,
574577
CancellationToken cancellationToken = default) where T : class
575578
{
576-
return await ChunkedBatchAsync(indexName, objects, createIfNotExists ? Action.PartialUpdateObject : Action.PartialUpdateObjectNoCreate, 1000, options, cancellationToken).ConfigureAwait(false);
579+
return await ChunkedBatchAsync(indexName, objects, createIfNotExists ? Action.PartialUpdateObject : Action.PartialUpdateObjectNoCreate, false, 1000, options, cancellationToken).ConfigureAwait(false);
577580
}
578581

579582
private static async Task<List<TU>> CreateIterable<TU>(Func<TU, Task<TU>> executeQuery,

clients/algoliasearch-client-swift/Sources/Search/Extra/SearchClientExtension.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -502,19 +502,20 @@ public extension SearchClient {
502502
/// `chunkedBatch` helper is used under the hood, which creates a `batch` requests with at most 1000 objects in it.
503503
/// - parameter indexName: The name of the index where to update the objects
504504
/// - parameter objects: The objects to update
505-
/// - parameter createIfNotExist: To be provided if non-existing objects are passed, otherwise, the call will fail..
505+
/// - parameter createIfNotExists: To be provided if non-existing objects are passed, otherwise, the call will
506+
/// fail..
506507
/// - parameter requestOptions: The request options
507508
/// - returns: [BatchResponse]
508509
func partialUpdateObjects(
509510
indexName: String,
510511
objects: [some Encodable],
511-
createIfNotExist: Bool = false,
512+
createIfNotExists: Bool = false,
512513
requestOptions: RequestOptions? = nil
513514
) async throws -> [BatchResponse] {
514515
try await self.chunkedBatch(
515516
indexName: indexName,
516517
objects: objects,
517-
action: createIfNotExist ? .partialUpdateObject : .partialUpdateObjectNoCreate,
518+
action: createIfNotExists ? .partialUpdateObject : .partialUpdateObjectNoCreate,
518519
waitForTasks: false,
519520
batchSize: 1000,
520521
requestOptions: requestOptions
@@ -544,7 +545,7 @@ public extension SearchClient {
544545
operationIndexParams: OperationIndexParams(
545546
operation: .copy,
546547
destination: tmpIndexName,
547-
scope: [.rules, .settings, .synonyms]
548+
scope: [.settings, .rules, .synonyms]
548549
),
549550
requestOptions: requestOptions
550551
)
@@ -563,7 +564,7 @@ public extension SearchClient {
563564
operationIndexParams: OperationIndexParams(
564565
operation: .copy,
565566
destination: tmpIndexName,
566-
scope: [.rules, .settings, .synonyms]
567+
scope: [.settings, .rules, .synonyms]
567568
),
568569
requestOptions: requestOptions
569570
)

generators/src/main/java/com/algolia/codegen/cts/tests/TestsClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import com.algolia.codegen.exceptions.CTSException;
66
import com.algolia.codegen.utils.*;
7+
import io.swagger.util.Json;
78
import java.io.File;
89
import java.util.ArrayList;
910
import java.util.HashMap;
@@ -160,7 +161,11 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
160161
if (step.expected.match instanceof Map match) {
161162
paramsType.enhanceParameters(match, matchMap);
162163
stepOut.put("match", matchMap);
163-
stepOut.put("matchIsObject", true);
164+
stepOut.put("matchIsJSON", true);
165+
} else if (step.expected.match instanceof List match) {
166+
matchMap.put("parameters", Json.mapper().writeValueAsString(step.expected.match));
167+
stepOut.put("match", matchMap);
168+
stepOut.put("matchIsJSON", true);
164169
} else {
165170
stepOut.put("match", step.expected.match);
166171
}

scripts/cts/runCts.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { createSpinner } from '../spinners.js';
55
import type { Language } from '../types.js';
66

77
import { startTestServer } from './testServer';
8+
import { assertChunkWrapperValid } from './testServer/chunkWrapper.js';
9+
import { assertValidReplaceAllObjects } from './testServer/replaceAllObjects.js';
810
import { assertValidTimeouts } from './testServer/timeout.js';
911

1012
async function runCtsOne(language: string): Promise<void> {
@@ -86,9 +88,10 @@ export async function runCts(languages: Language[], clients: string[]): Promise<
8688
if (useTestServer) {
8789
await close();
8890

89-
assertValidTimeouts(languages.length);
91+
const skip = (lang: Language): number => (languages.includes(lang) ? 1 : 0);
9092

91-
// uncomment this once all languages are supported
92-
// assertValidReplaceAllObjects(languages.length);
93+
assertValidTimeouts(languages.length);
94+
assertChunkWrapperValid(languages.length - skip('dart') - skip('scala'));
95+
assertValidReplaceAllObjects(languages.length - skip('dart') - skip('scala'));
9396
}
9497
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { Server } from 'http';
2+
3+
import { expect } from 'chai';
4+
import express from 'express';
5+
import type { Express } from 'express';
6+
7+
import { setupServer } from '.';
8+
9+
const chunkWrapperState: Record<string, any> = {};
10+
11+
export function assertChunkWrapperValid(expectedCount: number): void {
12+
if (Object.values(chunkWrapperState).length !== expectedCount) {
13+
throw new Error('unexpected number of call to chunkWrapper');
14+
}
15+
for (const state of Object.values(chunkWrapperState)) {
16+
expect(state).to.deep.equal({ saveObjects: 1, partialUpdateObjects: 2, deleteObjects: 1 });
17+
}
18+
}
19+
20+
function addRoutes(app: Express): void {
21+
app.use(express.urlencoded({ extended: true }));
22+
app.use(
23+
express.json({
24+
type: ['application/json', 'text/plain'], // the js client sends the body as text/plain
25+
}),
26+
);
27+
28+
app.post('/1/indexes/:indexName/batch', (req, res) => {
29+
const match = req.params.indexName.match(/^cts_e2e_(\w+)_(.*)$/);
30+
const helper = match?.[1] as string;
31+
const lang = match?.[2] as string;
32+
33+
if (!chunkWrapperState[lang]) {
34+
chunkWrapperState[lang] = {};
35+
}
36+
chunkWrapperState[lang][helper] = (chunkWrapperState[lang][helper] ?? 0) + 1;
37+
switch (helper) {
38+
case 'saveObjects':
39+
expect(req.body).to.deep.equal({
40+
requests: [
41+
{ action: 'addObject', body: { objectID: '1', name: 'Adam' } },
42+
{ action: 'addObject', body: { objectID: '2', name: 'Benoit' } },
43+
],
44+
});
45+
46+
res.json({
47+
taskID: 333,
48+
objectIDs: req.body.requests.map((r) => r.body.objectID),
49+
});
50+
51+
break;
52+
case 'partialUpdateObjects':
53+
if (req.body.requests[0].body.objectID === '1') {
54+
expect(req.body).to.deep.equal({
55+
requests: [
56+
{ action: 'partialUpdateObject', body: { objectID: '1', name: 'Adam' } },
57+
{ action: 'partialUpdateObject', body: { objectID: '2', name: 'Benoit' } },
58+
],
59+
});
60+
61+
res.json({
62+
taskID: 444,
63+
objectIDs: req.body.requests.map((r) => r.body.objectID),
64+
});
65+
} else {
66+
expect(req.body).to.deep.equal({
67+
requests: [
68+
{ action: 'partialUpdateObjectNoCreate', body: { objectID: '3', name: 'Cyril' } },
69+
{ action: 'partialUpdateObjectNoCreate', body: { objectID: '4', name: 'David' } },
70+
],
71+
});
72+
73+
res.json({
74+
taskID: 555,
75+
objectIDs: req.body.requests.map((r) => r.body.objectID),
76+
});
77+
}
78+
break;
79+
case 'deleteObjects':
80+
expect(req.body).to.deep.equal({
81+
requests: [
82+
{ action: 'deleteObject', body: { objectID: '1' } },
83+
{ action: 'deleteObject', body: { objectID: '2' } },
84+
],
85+
});
86+
87+
res.json({
88+
taskID: 666,
89+
objectIDs: req.body.requests.map((r) => r.body.objectID),
90+
});
91+
break;
92+
default:
93+
throw new Error('unknown helper');
94+
}
95+
});
96+
97+
// fallback route
98+
app.use((req, res) => {
99+
// eslint-disable-next-line no-console
100+
console.log('fallback route', req.method, req.url);
101+
res.status(404).json({ message: 'not found' });
102+
});
103+
104+
app.use((err, req, res, _) => {
105+
// eslint-disable-next-line no-console
106+
console.error(err.message);
107+
res.status(500).send({ message: err.message });
108+
});
109+
}
110+
111+
export function chunkWrapperServer(): Promise<Server> {
112+
return setupServer('chunkWrapper', 6680, addRoutes);
113+
}

scripts/cts/testServer/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Express } from 'express';
55

66
import { createSpinner } from '../../spinners';
77

8+
import { chunkWrapperServer } from './chunkWrapper';
89
import { gzipServer } from './gzip';
910
import { replaceAllObjectsServer } from './replaceAllObjects';
1011
import { timeoutServer } from './timeout';
@@ -13,9 +14,10 @@ import { timeoutServerBis } from './timeoutBis';
1314
export async function startTestServer(): Promise<() => Promise<void>> {
1415
const servers = await Promise.all([
1516
timeoutServer(),
16-
timeoutServerBis(),
1717
gzipServer(),
18+
timeoutServerBis(),
1819
replaceAllObjectsServer(),
20+
chunkWrapperServer(),
1921
]);
2022

2123
return async () => {

0 commit comments

Comments
 (0)