PromucFlow_constructor/app/client/cypress/scripts/util.ts
Saroj cf53dbfb23
ci: Allow static split of cypress specs (#29653)
## Description
- Allowing static split of cypress specs in CI runs.
- Optimised the spec allocation by considering the duration history for
each spec.
- Updated the affecting workflows to utilise the static split

#### Type of change
- Workflows
- Cypress-split pugin
## Testing
- Workflow run

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new environment variable to optimize test resource
allocation during CI/CD processes.
- Enhanced Cypress testing with dynamic and static test splitting
strategies.

- **Refactor**
- Refactored Cypress plugin configuration to support conditional test
splitting based on the environment variable.

- **Chores**
- Updated GitHub Actions workflows to include new environment variables
for test runs.

- **Documentation**
- Updated internal documentation to reflect changes in test setup and
execution strategies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2023-12-18 17:10:11 +05:30

183 lines
6.1 KiB
TypeScript

import { Pool } from "pg";
import { S3 } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import fs from "fs";
import { Octokit } from "@octokit/rest";
import fetch from "node-fetch";
import globby from "globby";
import minimatch from "minimatch";
export interface DataItem {
name: string;
duration: string;
}
export default class util {
public getVars() {
return {
runId: this.getEnvValue("RUNID", { required: true }),
attempt_number: this.getEnvValue("ATTEMPT_NUMBER", { required: true }),
repository: this.getEnvValue("REPOSITORY", { required: true }),
committer: this.getEnvValue("COMMITTER", { required: true }),
tag: this.getEnvValue("TAG", { required: true }),
branch: this.getEnvValue("BRANCH", { required: true }),
cypressDbUser: this.getEnvValue("CYPRESS_DB_USER", { required: true }),
cypressDbHost: this.getEnvValue("CYPRESS_DB_HOST", { required: true }),
cypressDbName: this.getEnvValue("CYPRESS_DB_NAME", { required: true }),
cypressDbPwd: this.getEnvValue("CYPRESS_DB_PWD", { required: true }),
cypressS3Access: this.getEnvValue("CYPRESS_S3_ACCESS", {
required: true,
}),
cypressS3Secret: this.getEnvValue("CYPRESS_S3_SECRET", {
required: true,
}),
githubToken: process.env["GITHUB_TOKEN"],
commitMsg: this.getEnvValue("COMMIT_INFO_MESSAGE", { required: false }),
totalRunners: this.getEnvValue("TOTAL_RUNNERS", { required: false }),
thisRunner: this.getEnvValue("THIS_RUNNER", { required: true }),
cypressSpecs: this.getEnvValue("CYPRESS_SPECS", { required: false }),
cypressRerun: this.getEnvValue("CYPRESS_RERUN", { required: false }),
cypressSkipFlaky: this.getEnvValue("CYPRESS_SKIP_FLAKY", {
required: false,
}),
staticAllocation: this.getEnvValue("CYPRESS_STATIC_ALLOCATION", {
required: false,
}),
};
}
public async divideSpecsIntoBalancedGroups(
data: DataItem[],
numberOfGroups: number,
): Promise<DataItem[][]> {
const groups: DataItem[][] = Array.from(
{ length: numberOfGroups },
() => [],
);
data.forEach((item) => {
// Find the group with the shortest total duration and add the item to it
const shortestGroupIndex = groups.reduce(
(minIndex, group, currentIndex) => {
const totalDuration = groups[minIndex].reduce(
(acc, item) => acc + Number(item.duration),
0,
);
const totalDurationCurrent = group.reduce(
(acc, item) => acc + Number(item.duration),
0,
);
return totalDurationCurrent < totalDuration ? currentIndex : minIndex;
},
0,
);
groups[shortestGroupIndex].push(item);
});
return groups;
}
// This function will get all the spec paths using the pattern
public async getSpecFilePaths(
specPattern: any,
ignoreTestFiles: any,
): Promise<string[]> {
const files = globby.sync(specPattern, {
ignore: ignoreTestFiles,
});
// ignore the files that doesn't match
const ignorePatterns = [...(ignoreTestFiles || [])];
// a function which returns true if the file does NOT match
const doesNotMatchAllIgnoredPatterns = (file: string) => {
// using {dot: true} here so that folders with a '.' in them are matched
const MINIMATCH_OPTIONS = { dot: true, matchBase: true };
return ignorePatterns.every((pattern) => {
return !minimatch(file, pattern, MINIMATCH_OPTIONS);
});
};
const filtered = files.filter(doesNotMatchAllIgnoredPatterns);
return filtered;
}
public getEnvValue(varName: string, { required = true }): string {
if (required && process.env[varName] === undefined) {
throw Error(
`${varName} is not defined.
Please check all the following environment variables are defined properly
[ RUNID, ATTEMPT_NUMBER, REPOSITORY, COMMITTER, TAG, BRANCH, THIS_RUNNER, CYPRESS_DB_USER, CYPRESS_DB_HOST, CYPRESS_DB_NAME, CYPRESS_DB_PWD, CYPRESS_S3_ACCESS, CYPRESS_S3_SECRET ].`,
);
}
return process.env[varName] ?? "";
}
//This is to setup the db client
public configureDbClient() {
const dbConfig = {
user: this.getVars().cypressDbUser,
host: this.getVars().cypressDbHost,
database: this.getVars().cypressDbName,
password: this.getVars().cypressDbPwd,
port: 5432,
connectionTimeoutMillis: 60000,
ssl: true,
keepalives: 30,
};
const dbClient = new Pool(dbConfig);
return dbClient;
}
// This is to setup the AWS client
public configureS3() {
const s3client = new S3({
region: "ap-south-1",
credentials: {
accessKeyId: this.getVars().cypressS3Access,
secretAccessKey: this.getVars().cypressS3Secret,
},
});
return s3client;
}
// This is to upload files to s3 when required
public async uploadToS3(s3Client: S3, filePath: string, key: string) {
const fileContent = fs.readFileSync(filePath);
const params = {
Bucket: "appsmith-internal-cy-db",
Key: key,
Body: fileContent,
};
return await new Upload({ client: s3Client, params }).done();
}
public async getActiveRunners() {
const octokit = new Octokit({
auth: this.getVars().githubToken,
request: {
fetch: fetch,
},
});
try {
const repo: string[] = this.getVars().repository.split("/");
const response = await octokit.request(
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
{
owner: repo[0],
repo: repo[1],
run_id: Number(this.getVars().runId),
per_page: 100,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
);
const active_runners = response.data.jobs.filter(
(job) =>
(job.status === "in_progress" || job.status === "queued") &&
job.run_attempt === Number(this.getVars().attempt_number),
);
return active_runners.length;
} catch (error) {
console.error("Error:", error);
}
}
}