Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13072-tests-1762800216646.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Fix flakey stackscript tests ([#13072](https://github.com/linode/manager/pull/13072))
2 changes: 2 additions & 0 deletions packages/manager/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { discardPassedTestRecordings } from './cypress/support/plugins/discard-p
import { featureFlagOverrides } from './cypress/support/plugins/feature-flag-override';
import { fetchAccount } from './cypress/support/plugins/fetch-account';
import { fetchLinodeClusters } from './cypress/support/plugins/fetch-linode-clusters';
import { fetchLinodeImages } from './cypress/support/plugins/fetch-linode-images';
import { fetchLinodeRegions } from './cypress/support/plugins/fetch-linode-regions';
import { generateTestWeights } from './cypress/support/plugins/generate-weights';
import { enableHtmlReport } from './cypress/support/plugins/html-report';
Expand Down Expand Up @@ -102,6 +103,7 @@ export default defineConfig({
fetchAccount,
fetchLinodeRegions,
fetchLinodeClusters,
fetchLinodeImages,
resetUserPreferences,
regionOverrideCheck,
clusterOverrideCheck,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { ui } from 'support/ui';
import { SimpleBackoffMethod } from 'support/util/backoff';
import { cleanUp } from 'support/util/cleanup';
import { chooseImage } from 'support/util/images';
import { createTestLinode } from 'support/util/linodes';
import {
pollImageStatus,
Expand Down Expand Up @@ -185,8 +186,9 @@ describe('Create stackscripts', () => {
it('creates a StackScript and deploys a Linode with it', () => {
const stackscriptLabel = randomLabel();
const stackscriptDesc = randomPhrase();
const stackscriptImage = 'Alpine 3.19';

// use random image. can specify image w/ getImageByLabel, then set images option in chooseImage
const randomImage = chooseImage();
const stackscriptImage = randomImage.label;
const linodeLabel = randomLabel();
const linodeRegion = chooseRegion({ capabilities: ['Vlans'] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ui } from 'support/ui';
import { depaginate } from 'support/util/paginate';
import { randomLabel, randomPhrase } from 'support/util/random';

import { isImageDeprecated } from 'src/components/ImageSelect/utilities';

import type { Image, StackScript } from '@linode/api-v4';

// StackScript fixture paths.
Expand Down Expand Up @@ -98,7 +100,7 @@ describe('Update stackscripts', () => {
getImages({ page }, { is_public: true })
);
return allPublicImages.find(
(image) => image.vendor === 'Alpine' && image.deprecated === false
(image) => image.vendor === 'Alpine' && !isImageDeprecated(image)
);
};

Expand Down
20 changes: 20 additions & 0 deletions packages/manager/cypress/support/plugins/fetch-linode-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getImages } from '@linode/api-v4';

import type { CypressPlugin } from './plugin';
import type { Image, ResourcePage } from '@linode/api-v4';

/**
* Fetches and stores Linode image data in Cypress environment object.
*/
export const fetchLinodeImages: CypressPlugin = async (_, config) => {
const data: ResourcePage<Image> = await getImages({ page_size: 500 });

const images = data.data;
return {
...config,
env: {
...config.env,
cloudManagerImages: images,
},
};
};
138 changes: 138 additions & 0 deletions packages/manager/cypress/support/util/images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { randomItem } from 'support/util/random';

import { isImageDeprecated } from 'src/components/ImageSelect/utilities';

import type { Image, ImageCapabilities } from '@linode/api-v4';
/**
* Images that cannot be selected using `chooseImages()`.
*/
const disallowedImageIds: string[] = [];

/**
* All Linode images available to the current Cloud Manager user.
*
* Retrieved via Linode APIv4 during Cypress start-up.
*/
export const images: Image[] = Cypress.env('cloudManagerImages') as Image[];

/**
* Returns a known Cloud Manager image at random, or returns a user-chosen
* image if one was specified.
*
* @param options - Image selection options.
*
* @returns Object describing a Cloud Manager image to use during tests.
*/
export const chooseImage = (options?: ChooseImageOptions): Image => {
return randomItem(resolveSearchImages(options));
};

/**
* Returns an array of Image objects that meet the given criteria.
*
* @param options - Object describing Image selection criteria.
*
* @throws If no images meet the desired criteria.
* @throws If an override image is specified which does not meet the given criteria.
*
* @returns Array of Image objects that meet criteria specified by `options` param.
*/
const resolveSearchImages = (options?: ChooseImageOptions): Image[] => {
const imageFixtures = options?.images ?? images;
const currentImages = imageFixtures.filter(
(image) => !isImageDeprecated(image)
);
const requiredCapabilities = options?.capabilities ?? [];
const allDisallowedImageIds = [
...disallowedImageIds,
...(options?.exclude ?? []),
];
const capableImages = imagesWithCapabilities(
currentImages,
requiredCapabilities
).filter((image: Image) => !allDisallowedImageIds.includes(image.id));

if (!capableImages.length) {
throw new Error(
`No images are available with the required capabilities: ${requiredCapabilities.join(
', '
)}`
);
}
return capableImages;
};

/**
* Returns `true` if the given Image has all of the given capabilities and availability for each capability.
*
* @param image - Image to check capabilities.
* @param capabilities - ImageCapabilities to check.
*
* @returns `true` if `image` has all of the given capabilities.
*/
const imageHasCapabilities = (
image: Image,
capabilities: ImageCapabilities[]
): boolean => {
return capabilities.every((capability) =>
image.capabilities.includes(capability)
);
};

/**
* Returns an array of Image objects that have all of the given capabilities.
*
* @param images - Images from which to search.
* @param capabilities - ImageCapabilities to check.
*
* @returns Array of Image objects containing the required capabilities.
*/
const imagesWithCapabilities = (
images: Image[],
capabilities: ImageCapabilities[]
): Image[] => {
return images.filter((image: Image) =>
imageHasCapabilities(image, capabilities)
);
};

/**
* Returns an object describing a Cloud Manager image with the given label.
*
* If no known image exists with the given human-readable label, an error is
* thrown.
*
* @param label - Label (API or Cloud-specific) of the image to find.
* @param searchImages - Optional array of Images from which to search.
*
* @throws When no image exists in the `images` array with the given label.
*/
export const getImageByLabel = (label: string, searchImages?: Image[]) => {
const image = (searchImages ?? images).find(
(findImage: Image) => findImage.label === label
);
if (!image) {
throw new Error(
`Unable to find image by label. Unknown image label '${label}'.`
);
}
return image;
};

interface ChooseImageOptions {
/**
* If specified, the image returned will support the defined capabilities
* @example ['cloud-init', 'distributed-sites']
*/
capabilities?: ImageCapabilities[];

/**
* Array of image IDs to exclude from results.
*/
exclude?: string[];

/**
* Images from which to choose. If unspecified, Images exposed by the API will be used.
*/
images?: Image[];
}