Skip to content

Commit

Permalink
test: [M3-7852] - Use test weights for Jenkins/Cypress parallelization (
Browse files Browse the repository at this point in the history
#10310)

* Allow test weights files to be generated when running Cypress tests

* Allow test weights file to be read when distributing Cypress tests across multiple runners

* Document `CY_TEST_SPLIT_RUN_WEIGHTS` and `CY_TEST_GENWEIGHTS`
  • Loading branch information
jdamore-linode authored Aug 26, 2024
1 parent 6821a56 commit 6cb89c6
Show file tree
Hide file tree
Showing 4 changed files with 408 additions and 55 deletions.
28 changes: 15 additions & 13 deletions docs/development-guide/08-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,24 +199,26 @@ These environment variables are used by Cloud Manager's UI tests to override reg
| `CY_TEST_REGION` | ID of region to test (as used by Linode APIv4). | `us-east` | Unset; regions are selected at random |
###### Run Splitting
These environment variables facilitate splitting the Cypress run between multiple runners without the use of any third party services. This can be useful for improving Cypress test performance in some circumstances.
These environment variables facilitate splitting the Cypress run between multiple runners without the use of any third party services. This can be useful for improving Cypress test performance in some circumstances. For additional performance gains, an optional test weights file can be specified using `CY_TEST_SPLIT_RUN_WEIGHTS` (see `CY_TEST_GENWEIGHTS` to generate test weights).
| Environment Variable | Description | Example | Default |
|---------------------------|--------------------------------------------|----------------|----------------------------|
| `CY_TEST_SPLIT_RUN` | Enable run splitting | `1` | Unset; disabled by default |
| `CY_TEST_SPLIT_RUN_INDEX` | Numeric index for each Cypress test runner | `1`, `2`, etc. | Unset |
| `CY_TEST_SPLIT_RUN_TOTAL` | Total number of runners for the tests | `2` | Unset |
| Environment Variable | Description | Example | Default |
|-----------------------------|--------------------------------------------|------------------|----------------------------|
| `CY_TEST_SPLIT_RUN` | Enable run splitting | `1` | Unset; disabled by default |
| `CY_TEST_SPLIT_RUN_INDEX` | Numeric index for each Cypress test runner | `1`, `2`, etc. | Unset |
| `CY_TEST_SPLIT_RUN_TOTAL` | Total number of runners for the tests | `2` | Unset |
| `CY_TEST_SPLIT_RUN_WEIGHTS` | Path to test weights file | `./weights.json` | Unset; disabled by default |
###### Development, Logging, and Reporting
Environment variables related to Cypress logging and reporting, as well as report generation.
| Environment Variable | Description | Example | Default |
|---------------------------------|-----------------------------------------------|-----------|----------------------------|
| `CY_TEST_USER_REPORT` | Log test account information when tests begin | `1` | Unset; disabled by default |
| `CY_TEST_JUNIT_REPORT` | Enable JUnit reporting | `1` | Unset; disabled by default |
| `CY_TEST_DISABLE_FILE_WATCHING` | Disable file watching in Cypress UI | `1` | Unset; disabled by default |
| `CY_TEST_DISABLE_RETRIES` | Disable test retries on failure in CI | `1` | Unset; disabled by default |
| `CY_TEST_FAIL_ON_MANAGED` | Fail affected tests when Managed is enabled | `1` | Unset; disabled by default |
| Environment Variable | Description | Example | Default |
|---------------------------------|----------------------------------------------------|------------------|----------------------------|
| `CY_TEST_USER_REPORT` | Log test account information when tests begin | `1` | Unset; disabled by default |
| `CY_TEST_JUNIT_REPORT` | Enable JUnit reporting | `1` | Unset; disabled by default |
| `CY_TEST_DISABLE_FILE_WATCHING` | Disable file watching in Cypress UI | `1` | Unset; disabled by default |
| `CY_TEST_DISABLE_RETRIES` | Disable test retries on failure in CI | `1` | Unset; disabled by default |
| `CY_TEST_FAIL_ON_MANAGED` | Fail affected tests when Managed is enabled | `1` | Unset; disabled by default |
| `CY_TEST_GENWEIGHTS` | Generate and output test weights to the given path | `./weights.json` | Unset; disabled by default |
### Writing End-to-End Tests
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { fetchAccount } from './cypress/support/plugins/fetch-account';
import { fetchLinodeRegions } from './cypress/support/plugins/fetch-linode-regions';
import { splitCypressRun } from './cypress/support/plugins/split-run';
import { enableJunitReport } from './cypress/support/plugins/junit-report';
import { generateTestWeights } from './cypress/support/plugins/generate-weights';
import { logTestTagInfo } from './cypress/support/plugins/test-tagging-info';

/**
Expand Down Expand Up @@ -70,6 +71,7 @@ export default defineConfig({
logTestTagInfo,
splitCypressRun,
enableJunitReport,
generateTestWeights,
]);
},
},
Expand Down
155 changes: 155 additions & 0 deletions packages/manager/cypress/support/plugins/generate-weights.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type { CypressPlugin } from './plugin';
import { DateTime } from 'luxon';
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import { object, string, array, number, SchemaOf } from 'yup';

// The name of the environment variable to read to check if generation is enabled.
// The value should be a path to the weights file.
const envVarName = 'CY_TEST_GENWEIGHTS';

/**
* Describes spec file weights for a test suite.
*/
export interface SpecWeights {
/**
* Spec weight metadata.
*/
meta: {
/**
* Date and time that test spec weights were generated.
*/
datetime: string;

/**
* Total test weight.
*/
totalWeight: number;

/**
* Total test run duration in milliseconds.
*/
totalDuration: number;
};
/**
* Array of spec weights.
*/
weights: SpecWeight[];
}

/**
* Describes the weight of an individual spec file.
*/
export interface SpecWeight extends SpecResult {
/**
* Spec weight.
*/
weight: number;
}

/**
* Spec weights schema for JSON parsing, etc.
*/
export const specWeightsSchema: SchemaOf<SpecWeights> = object({
meta: object({
datetime: string().required(),
totalWeight: number().required(),
totalDuration: number().required(),
}).required(),
weights: array(
object({
filepath: string().required(),
duration: number().required(),
weight: number().required(),
})
).required(),
});

/**
* Describes the duration of an individual spec file.
*
* Used in the process of calculating weights for each spec.
*/
interface SpecResult {
/**
* Relative path to spec file.
*/
filepath: string;

/**
* Spec run duration in milliseconds.
*/
duration: number;
}

/**
* Enables test weight generation when `CY_TEST_GENWEIGHTS` is defined.
*
* @returns Cypress configuration object.
*/
export const generateTestWeights: CypressPlugin = (on, config) => {
const specResults: SpecResult[] = [];

if (!!config.env[envVarName]) {
const writeFilepath = config.env[envVarName];

// Capture duration after each spec runs.
on('after:spec', (spec, results) => {
const duration = results.stats.duration;
if (duration) {
specResults.push({
filepath: spec.relative,
duration,
});
} else {
console.warn(
`Failed to record test information for '${spec.relative}'`
);
}
});

// Aggregate spec durations and save as a spec weights JSON file.
on(
'after:run',
(
results:
| CypressCommandLine.CypressRunResult
| CypressCommandLine.CypressFailedRunResult
) => {
// Determine whether this is a failed run. "Failed" in this context means
// that Cypress itself failed to run, not that the test results contained failures.
const isFailedResult = (
results:
| CypressCommandLine.CypressRunResult
| CypressCommandLine.CypressFailedRunResult
): results is CypressCommandLine.CypressFailedRunResult => {
return 'failures' in results;
};

if (!isFailedResult(results)) {
const totalWeight = 100;
const totalDuration = results.totalDuration;
const weights: SpecWeights = {
meta: {
datetime: DateTime.now().toISO(),
totalWeight,
totalDuration,
},
weights: specResults.map(
(specResult: SpecResult): SpecWeight => {
return {
filepath: specResult.filepath,
duration: specResult.duration,
weight: (specResult.duration / totalDuration) * totalWeight,
};
}
),
};

const resolvePath = resolve(writeFilepath);
writeFileSync(resolvePath, JSON.stringify(weights), 'utf-8');
}
}
);
}
};
Loading

0 comments on commit 6cb89c6

Please sign in to comment.