Skip to content

Commit 4e7e670

Browse files
authored
Merge pull request #339 from LambdaTest/stage
Release PR version `4.1.26` Sync functionality in SDK
2 parents c5dc388 + 8edad96 commit 4e7e670

File tree

8 files changed

+259
-13
lines changed

8 files changed

+259
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.1.25",
3+
"version": "4.1.26",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/lib/ctx.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ export default (options: Record<string, string>): Context => {
101101
useLambdaInternal = true;
102102
}
103103

104+
//if config.waitForPageRender has value and if its less than 30000 then make it to 30000 default
105+
if (config.waitForPageRender && config.waitForPageRender < 30000) {
106+
config.waitForPageRender = 30000;
107+
}
108+
104109
return {
105110
env: env,
106111
log: logger,

src/lib/httpClient.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export default class httpClient {
321321
}, ctx.log)
322322
}
323323

324-
processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, discoveryErrors: DiscoveryErrors) {
324+
processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) {
325325
return this.request({
326326
url: `/build/${ctx.build.id}/snapshot`,
327327
method: 'POST',
@@ -330,12 +330,14 @@ export default class httpClient {
330330
name: snapshot.name,
331331
url: snapshot.url,
332332
snapshotUuid: snapshotUuid,
333+
variantCount: variantCount,
333334
test: {
334335
type: ctx.testType,
335336
source: 'cli'
336337
},
337338
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
338339
discoveryErrors: discoveryErrors,
340+
sync: sync
339341
}
340342
}, ctx.log)
341343
}
@@ -623,4 +625,14 @@ export default class httpClient {
623625
data: requestData
624626
}, ctx.log)
625627
}
628+
629+
getSnapshotStatus(snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
630+
return this.request({
631+
url: `/snapshot/status?buildId=${ctx.build.id}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
632+
method: 'GET',
633+
headers: {
634+
'Content-Type': 'application/json',
635+
}
636+
}, ctx.log);
637+
}
626638
}

src/lib/schemaValidation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,18 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
501501
sessionId: {
502502
type: "string",
503503
errorMessage: "Invalid snapshot options; sessionId must be a string"
504+
},
505+
contextId: {
506+
type: "string",
507+
errorMessage: "Invalid snapshot options; contextId must be a string"
508+
},
509+
sync: {
510+
type: "boolean",
511+
errorMessage: "Invalid snapshot options; sync must be a boolean"
512+
},
513+
timeout: {
514+
type: "number",
515+
errorMessage: "Invalid snapshot options; timeout must be a number"
504516
}
505517
},
506518
additionalProperties: false

src/lib/server.ts

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { validateSnapshot } from './schemaValidation.js'
77
import { pingIntervalId } from './utils.js';
88
import { startPolling } from './utils.js';
99

10+
const uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
1011
export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMessage, ServerResponse>> => {
1112

1213
const server: FastifyInstance<Server, IncomingMessage, ServerResponse> = fastify({
@@ -41,6 +42,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
4142
// Fetch sessionId from snapshot options if present
4243
const sessionId = snapshot?.options?.sessionId;
4344
let capsBuildId = ''
45+
const contextId = snapshot?.options?.contextId;
4446

4547
if (sessionId) {
4648
// Check if sessionId exists in the map
@@ -71,7 +73,23 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
7173
}
7274

7375
ctx.testType = testType;
74-
ctx.snapshotQueue?.enqueue(snapshot);
76+
77+
if (contextId && !ctx.contextToSnapshotMap) {
78+
ctx.contextToSnapshotMap = new Map();
79+
ctx.log.debug(`Initialized empty context mapping map for contextId: ${contextId}`);
80+
}
81+
82+
if (contextId && ctx.contextToSnapshotMap) {
83+
ctx.contextToSnapshotMap.set(contextId, 0);
84+
ctx.log.debug(`Marking contextId as captured and added to queue: ${contextId}`);
85+
}
86+
87+
if(contextId){
88+
ctx.snapshotQueue?.enqueueFront(snapshot);
89+
}else{
90+
ctx.snapshotQueue?.enqueue(snapshot);
91+
}
92+
7593
ctx.isSnapshotCaptured = true;
7694
replyCode = 200;
7795
replyBody = { data: { message: "success", warnings: [] }};
@@ -105,8 +123,16 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
105123
if (ctx.server){
106124
ctx.server.close();
107125
}
108-
let resp = await ctx.client.getS3PreSignedURL(ctx);
109-
await ctx.client.uploadLogs(ctx, resp.data.url);
126+
127+
let uploadCLILogsToS3 = ctx?.config?.useLambdaInternal || uploadDomToS3ViaEnv;
128+
if (!uploadCLILogsToS3) {
129+
ctx.log.debug(`Log file to be uploaded`)
130+
let resp = await ctx.client.getS3PreSignedURL(ctx);
131+
await ctx.client.uploadLogs(ctx, resp.data.url);
132+
} else {
133+
ctx.log.debug(`Log file to be uploaded via LSRS`)
134+
let resp = ctx.client.sendCliLogsToLSRS(ctx);
135+
}
110136

111137
if (pingIntervalId !== null) {
112138
clearInterval(pingIntervalId);
@@ -130,7 +156,113 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
130156
reply.code(200).send({ status: 'Server is running', version: ctx.cliVersion });
131157
});
132158

133-
159+
// Get snapshot status
160+
server.get('/snapshot/status', opts, async (request, reply) => {
161+
let replyCode: number;
162+
let replyBody: Record<string, any>;
163+
164+
165+
try {
166+
ctx.log.debug(`request.query : ${JSON.stringify(request.query)}`);
167+
const { contextId, pollTimeout, snapshotName } = request.query as { contextId: string, pollTimeout: number, snapshotName: string };
168+
if (!contextId || !snapshotName) {
169+
throw new Error('contextId and snapshotName are required parameters');
170+
}
171+
172+
const timeoutDuration = pollTimeout*1000 || 30000;
173+
174+
// Check if we have stored snapshot status for this contextId
175+
if (ctx.contextToSnapshotMap?.has(contextId)) {
176+
let contextStatus = ctx.contextToSnapshotMap.get(contextId);
177+
178+
while (contextStatus==0) {
179+
// Wait 5 seconds before next check
180+
await new Promise(resolve => setTimeout(resolve, 5000));
181+
182+
contextStatus = ctx.contextToSnapshotMap.get(contextId);
183+
}
184+
185+
if(contextStatus==2){
186+
throw new Error("Snapshot Failed");
187+
}
188+
189+
ctx.log.debug("Snapshot uploaded successfully");
190+
191+
// Poll external API until it returns 200 or timeout is reached
192+
let lastExternalResponse: any = null;
193+
const startTime = Date.now();
194+
195+
while (true) {
196+
try {
197+
const externalResponse = await ctx.client.getSnapshotStatus(
198+
snapshotName,
199+
contextId,
200+
ctx
201+
);
202+
203+
lastExternalResponse = externalResponse;
204+
205+
if (externalResponse.statusCode === 200) {
206+
replyCode = 200;
207+
replyBody = externalResponse.data;
208+
return reply.code(replyCode).send(replyBody);
209+
} else if (externalResponse.statusCode === 202 ) {
210+
replyBody= externalResponse.data;
211+
ctx.log.debug(`External API attempt: Still processing, Pending Screenshots ${externalResponse.snapshotCount}`);
212+
await new Promise(resolve => setTimeout(resolve, 5000));
213+
}else if(externalResponse.statusCode===404){
214+
ctx.log.debug(`Snapshot still processing, not uploaded`);
215+
await new Promise(resolve => setTimeout(resolve, 5000));
216+
}else {
217+
ctx.log.debug(`Unexpected response from external API: ${JSON.stringify(externalResponse)}`);
218+
replyCode = 500;
219+
replyBody = {
220+
error: {
221+
message: `Unexpected response from external API: ${externalResponse.statusCode}`,
222+
externalApiStatus: externalResponse.statusCode
223+
}
224+
};
225+
return reply.code(replyCode).send(replyBody);
226+
}
227+
228+
ctx.log.debug(`timeoutDuration: ${timeoutDuration}`);
229+
ctx.log.debug(`Time passed: ${Date.now() - startTime}`);
230+
231+
if (Date.now() - startTime > timeoutDuration) {
232+
replyCode = 202;
233+
replyBody = {
234+
data: {
235+
message: 'Request timed out-> Snapshot still processing'
236+
}
237+
};
238+
return reply.code(replyCode).send(replyBody);
239+
}
240+
241+
} catch (externalApiError: any) {
242+
ctx.log.debug(`External API call failed: ${externalApiError.message}`);
243+
replyCode = 500;
244+
replyBody = {
245+
error: {
246+
message: `External API call failed: ${externalApiError.message}`
247+
}
248+
};
249+
return reply.code(replyCode).send(replyBody);
250+
}
251+
}
252+
} else {
253+
// No snapshot found for this contextId
254+
replyCode = 404;
255+
replyBody = { error: { message: `No snapshot found for contextId: ${contextId}` } };
256+
return reply.code(replyCode).send(replyBody);
257+
}
258+
} catch (error: any) {
259+
ctx.log.debug(`snapshot status failed; ${error}`);
260+
replyCode = 500;
261+
replyBody = { error: { message: error.message } };
262+
return reply.code(replyCode).send(replyBody);
263+
}
264+
});
265+
134266

135267
await server.listen({ port: ctx.options.port });
136268
// store server's address for SDK

src/lib/snapshotQueue.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Snapshot, Context } from "../types.js";
33
import constants from "./constants.js";
44
import processSnapshot, {prepareSnapshot} from "./processSnapshot.js"
55
import { v4 as uuidv4 } from 'uuid';
6-
import { startPolling, stopTunnelHelper } from "./utils.js";
6+
import { startPolling, stopTunnelHelper, calculateVariantCountFromSnapshot } from "./utils.js";
77

88
const uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
99
export default class Queue {
@@ -29,6 +29,16 @@ export default class Queue {
2929
}
3030
}
3131

32+
enqueueFront(item: Snapshot): void {
33+
this.snapshots.unshift(item);
34+
if (!this.ctx.config.delayedUpload) {
35+
if (!this.processing) {
36+
this.processing = true;
37+
this.processNext();
38+
}
39+
}
40+
}
41+
3242
startProcessingfunc(): void {
3343
if (!this.processing) {
3444
this.processing = true;
@@ -129,6 +139,8 @@ export default class Queue {
129139
return drop;
130140
}
131141

142+
143+
132144
private filterVariants(snapshot: Snapshot, config: any): boolean {
133145
let allVariantsDropped = true;
134146

@@ -273,6 +285,7 @@ export default class Queue {
273285
this.processingSnapshot = snapshot?.name;
274286
let drop = false;
275287

288+
276289
if (this.ctx.isStartExec && !this.ctx.config.tunnel) {
277290
this.ctx.log.info(`Processing Snapshot: ${snapshot?.name}`);
278291
}
@@ -332,6 +345,7 @@ export default class Queue {
332345

333346

334347
if (useCapsBuildId) {
348+
this.ctx.log.info(`Using cached buildId: ${capsBuildId}`);
335349
if (useKafkaFlowCaps) {
336350
const snapshotUuid = uuidv4();
337351
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
@@ -378,18 +392,23 @@ export default class Queue {
378392
}
379393
}
380394
if (this.ctx.build && this.ctx.build.useKafkaFlow) {
381-
const snapshotUuid = uuidv4();
395+
let snapshotUuid = uuidv4();
382396
let snapshotUploadResponse
383-
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
384-
if (!uploadDomToS3) {
397+
if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)) {
398+
snapshotUuid = snapshot.options.contextId;
399+
}
400+
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
401+
if (!uploadDomToS3) {
385402
this.ctx.log.debug(`Uploading dom to S3 for snapshot using presigned URL`);
386403
const presignedResponse = await this.ctx.client.getS3PresignedURLForSnapshotUpload(this.ctx, processedSnapshot.name, snapshotUuid);
387404
const uploadUrl = presignedResponse.data.url;
388405
snapshotUploadResponse = await this.ctx.client.uploadSnapshotToS3(this.ctx, uploadUrl, processedSnapshot);
389-
} else {
406+
} else {
390407
this.ctx.log.debug(`Uploading dom to S3 for snapshot using LSRS`);
391408
snapshotUploadResponse = await this.ctx.client.sendDomToLSRS(this.ctx, processedSnapshot, snapshotUuid);
392-
}
409+
}
410+
411+
393412
if (!snapshotUploadResponse || Object.keys(snapshotUploadResponse).length === 0) {
394413
this.ctx.log.debug(`snapshot failed; Unable to upload dom to S3`);
395414
this.processedSnapshots.push({ name: snapshot?.name, error: `snapshot failed; Unable to upload dom to S3` });
@@ -403,11 +422,19 @@ export default class Queue {
403422
this.ctx.log.debug(`Closed browser context for snapshot ${snapshot.name}`);
404423
}
405424
}
425+
if(snapshot?.options?.contextId){
426+
this.ctx.contextToSnapshotMap?.set(snapshot?.options?.contextId,2);
427+
}
406428
this.processNext();
407429
} else {
408-
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors);
430+
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors,calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config),snapshot?.options?.sync);
431+
if(snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)){
432+
this.ctx.contextToSnapshotMap.set(snapshot.options.contextId, 1);
433+
}
434+
this.ctx.log.debug(`ContextId: ${snapshot?.options?.contextId} status set to uploaded`);
409435
}
410436
} else {
437+
this.ctx.log.info(`Uploading snapshot to S3`);
411438
await this.ctx.client.uploadSnapshot(this.ctx, processedSnapshot, discoveryErrors);
412439
}
413440
this.ctx.totalSnapshots++;
@@ -418,6 +445,9 @@ export default class Queue {
418445
} catch (error: any) {
419446
this.ctx.log.debug(`snapshot failed; ${error}`);
420447
this.processedSnapshots.push({ name: snapshot?.name, error: error.message });
448+
if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap) {
449+
this.ctx.contextToSnapshotMap.set(snapshot.options.contextId, 2);
450+
}
421451
}
422452
// Close open browser contexts and pages
423453
if (this.ctx.browser) {

0 commit comments

Comments
 (0)