Skip to content

Commit d2b92fc

Browse files
committed
Add NOT_VALIDATED in file validation status terminal statuses
1 parent 4fbb40a commit d2b92fc

File tree

8 files changed

+290
-9
lines changed

8 files changed

+290
-9
lines changed

src/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,14 @@ export const defaultYaml: any =
4646
}
4747
]
4848
}
49+
50+
export const testmanagerApiVersion = "2024-07-01-preview";
51+
52+
namespace BaseAPIRoute {
53+
export const featureFlag = "featureFlags";
54+
}
55+
56+
export namespace APIRoute {
57+
const latestVersion = "api-version="+testmanagerApiVersion;
58+
export const FeatureFlags = (flag: string) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`;
59+
}

src/main.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as Util from './engine/Util';
66
import { TestKind } from "./engine/TestKind";
77
import * as fs from 'fs';
88
import { isNullOrUndefined } from 'util';
9+
import { FeatureFlagService } from './services/FeatureFlagService';
10+
import { FeatureFlags } from './services/FeatureFlags';
911

1012
const resultFolder = 'loadTest';
1113
const reportZipFileName = 'report.zip';
@@ -39,7 +41,7 @@ async function run() {
3941
core.setFailed(err.message);
4042
}
4143
}
42-
async function getTestAPI(validate:boolean) {
44+
async function getTestAPI(validate:boolean, returnTestObj:boolean = false) {
4345
var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion;
4446
urlSuffix = baseURL+urlSuffix;
4547
let header = await map.getTestHeader();
@@ -74,7 +76,11 @@ async function getTestAPI(validate:boolean) {
7476
testObj.kind = testObj.testType;
7577
}
7678
var inputScriptFileInfo = testObj.kind == TestKind.URL ? testObj.inputArtifacts.urlTestConfigFileInfo :testObj.inputArtifacts.testScriptFileInfo;
79+
7780
if(validate){
81+
if (returnTestObj) {
82+
return [inputScriptFileInfo.validationStatus, testObj];
83+
}
7884
return inputScriptFileInfo.validationStatus;
7985
}
8086
else
@@ -185,9 +191,10 @@ async function uploadTestPlan()
185191
var startTime = new Date();
186192
var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd*60000);
187193
var validationStatus = "VALIDATION_INITIATED";
194+
var testObj;
188195
while(maxAllowedTime>(new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) {
189196
try{
190-
validationStatus = await getTestAPI(true);
197+
[validationStatus, testObj] = await getTestAPI(true, true);
191198
}
192199
catch(e:any) {
193200
retry--;
@@ -197,8 +204,21 @@ async function uploadTestPlan()
197204
}
198205
await util.sleep(5000);
199206
}
207+
console.log("Validation status of the test plan: "+ validationStatus);
200208
if(validationStatus == null || validationStatus == "VALIDATION_SUCCESS" ){
201209
console.log(`Validated test plan for the test successfully.`);
210+
211+
// Get errors from all files
212+
var fileErrors = util.getAllFileErrors(testObj);
213+
214+
if (Object.keys(fileErrors).length > 0) {
215+
console.log("Validation failed for the following files:");
216+
for (const [file, error] of Object.entries(fileErrors)) {
217+
console.log(`File: ${file}, Error: ${error}`);
218+
}
219+
throw new Error("Validation of one or more files failed. Please correct the errors and try again.");
220+
}
221+
202222
await createTestRun();
203223
}
204224
else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED")
@@ -213,7 +233,7 @@ async function uploadConfigFile()
213233
if(configFiles != undefined && configFiles.length > 0) {
214234
for (let filepath of configFiles) {
215235
let filename = map.getFileName(filepath);
216-
var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion;
236+
var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS);
217237
urlSuffix = baseURL+urlSuffix;
218238
let headers = await map.UploadAndValidateHeader();
219239
let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,filepath, true);
@@ -250,6 +270,15 @@ async function uploadZipArtifacts()
250270
let flagValidationPending = true;
251271
let zipInvalid = false;
252272
let zipFailureReason = "";
273+
274+
// TODO(harshanb): Remove this check once the feature flag is enabled by default.
275+
let isTestScriptFragmentEnabled = await FeatureFlagService.isFeatureEnabledAsync(FeatureFlags.enableTestScriptFragments, baseURL);
276+
let zipValidationTerminateStates = ["VALIDATION_SUCCESS", "VALIDATION_FAILURE"];
277+
if (isTestScriptFragmentEnabled) {
278+
// NOT_VALIDATED is a terminal state for the file validation and actual validation will be performed during test script upload
279+
zipValidationTerminateStates.push("NOT_VALIDATED");
280+
}
281+
253282
while(maxAllowedTime>(new Date()) && flagValidationPending) {
254283
var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion;
255284
urlSuffix = baseURL+urlSuffix;
@@ -263,7 +292,7 @@ async function uploadZipArtifacts()
263292
flagValidationPending = false;
264293
if (testObj && testObj.inputArtifacts && testObj.inputArtifacts.additionalFileInfo) {
265294
for(const file of testObj.inputArtifacts.additionalFileInfo){
266-
if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus != "VALIDATION_SUCCESS" && file.validationStatus != "VALIDATION_FAILURE")) {
295+
if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus !in zipValidationTerminateStates)) {
267296
flagValidationPending = true;
268297
break;
269298
} else if(file.fileType == FileType.ZIPPED_ARTIFACTS && file.validationStatus == "VALIDATION_FAILURE"){
@@ -285,7 +314,12 @@ async function uploadZipArtifacts()
285314
} else if(flagValidationPending) {
286315
throw new Error("Validation of one or more zip artifacts timed out. Please retry.");
287316
}
288-
console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`);
317+
if (isTestScriptFragmentEnabled) {
318+
console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`);
319+
}
320+
else {
321+
console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`);
322+
}
289323
}
290324
var statuscode = await uploadPropertyFile();
291325
if(statuscode== 201){

src/mappers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ function getExistingData() {
101101
if (!envYaml.hasOwnProperty(key)) envYaml[key] = null;
102102
}
103103
}
104+
105+
export function getToken() {
106+
return token;
107+
}
108+
104109
export function createTestData() {
105110
getExistingData();
106111
var data = {

src/models/APIResponseModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface Definitions {
2+
/**
3+
* Response for a feature flag query
4+
*/
5+
FeatureFlagResponse: {
6+
featureFlag: string;
7+
enabled: boolean;
8+
};
9+
}

src/services/FeatureFlagService.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { FeatureFlags } from "./FeatureFlags";
2+
import { Definitions } from "../models/APIResponseModel";
3+
import { APIRoute } from "../constants";
4+
import * as map from '../mappers';
5+
import * as util from '../util';
6+
import { IHeaders } from "typed-rest-client/Interfaces";
7+
8+
export class FeatureFlagService {
9+
private static featureFlagCache: { [key: string]: boolean } = {};
10+
11+
public static async getFeatureFlagAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise<Definitions['FeatureFlagResponse'] | null> {
12+
if (useCache && flag in this.featureFlagCache) {
13+
return {featureFlag: flag, enabled: this.featureFlagCache[flag.toString()]};
14+
}
15+
16+
let uri: string = baseUrl + APIRoute.FeatureFlags(flag.toString());
17+
let headers: IHeaders = {
18+
'content-type': 'application/json',
19+
'Authorization': 'Bearer '+ map.getToken()
20+
};
21+
let flagResponse = await util.httpClientRetries(uri, headers, 'get', 3, "", false, false);
22+
try {
23+
let flagObj = (await util.getResultObj(flagResponse)) as Definitions["FeatureFlagResponse"];
24+
this.featureFlagCache[flag.toString()] = flagObj.enabled;
25+
return flagObj;
26+
}
27+
catch (error) {
28+
// remove item from dict
29+
// handle in case getFlag was called with cache true once and then with cache false, and failed during second call
30+
// remove the item from cache so that it can be fetched again rather than using old value
31+
delete this.featureFlagCache[flag.toString()];
32+
return null;
33+
}
34+
}
35+
36+
public static async isFeatureEnabledAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise<boolean> {
37+
let flagObj = await this.getFeatureFlagAsync(flag, baseUrl, useCache);
38+
return flagObj ? flagObj.enabled : false;
39+
}
40+
}

src/services/FeatureFlags.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum FeatureFlags {
2+
enableTestScriptFragments = "enableTestScriptFragments",
3+
}

src/util.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function uploadFileData(filepath: string) {
5454
}
5555

5656
const correlationHeader = 'x-ms-correlation-request-id'
57-
export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1, data : string, isUploadCall : boolean = true) : Promise<IHttpClientResponse>{
57+
export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1, data : string, isUploadCall : boolean = true, log: boolean = true) : Promise<IHttpClientResponse>{
5858
let httpResponse : IHttpClientResponse;
5959
try {
6060
let correlationId = `gh-actions-${getUniqueId()}`;
@@ -85,7 +85,9 @@ export async function httpClientRetries(urlSuffix : string, header : IHeaders, m
8585
if(retries){
8686
let sleeptime = (5-retries)*1000 + Math.floor(Math.random() * 5001);
8787
await sleep(sleeptime);
88-
console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`);
88+
if (log) {
89+
console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`);
90+
}
8991
return httpClientRetries(urlSuffix,header,method,retries-1,data);
9092
}
9193
else
@@ -490,4 +492,25 @@ export async function getResultObj(data:any) {
490492
}
491493
export function ErrorCorrection(result : IHttpClientResponse){
492494
return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code: " + result.message.statusCode ;
493-
}
495+
}
496+
497+
export function getAllFileErrors(testObj:any): { [key: string]: string } {
498+
var allArtifacts:any[] = [];
499+
for (var key in testObj.inputArtifacts) {
500+
var artifacts = testObj.inputArtifacts[key];
501+
if (artifacts instanceof Array ) {
502+
allArtifacts = allArtifacts.concat(artifacts.filter((artifact:any) => artifact !== null && artifact !== undefined));
503+
}
504+
else if (artifacts !== null && artifacts !== undefined) {
505+
allArtifacts.push(artifacts);
506+
}
507+
}
508+
509+
var fileErrors: { [key: string]: string } = {};
510+
for (const file of allArtifacts) {
511+
if (file.validationStatus === "VALIDATION_FAILURE") {
512+
fileErrors[file.fileName] = file.validationFailureDetails;
513+
}
514+
}
515+
return fileErrors;
516+
}

0 commit comments

Comments
 (0)