Skip to content

Commit aae562d

Browse files
test [UIE-9584]: fix flakey stackscript tests (#13072)
* initial commit * fix options.images. fix update-stackscripts * Added changeset: Fix flakey stackscript tests
1 parent a63d148 commit aae562d

File tree

6 files changed

+172
-3
lines changed

6 files changed

+172
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Tests
3+
---
4+
5+
Fix flakey stackscript tests ([#13072](https://github.com/linode/manager/pull/13072))

packages/manager/cypress.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { discardPassedTestRecordings } from './cypress/support/plugins/discard-p
1010
import { featureFlagOverrides } from './cypress/support/plugins/feature-flag-override';
1111
import { fetchAccount } from './cypress/support/plugins/fetch-account';
1212
import { fetchLinodeClusters } from './cypress/support/plugins/fetch-linode-clusters';
13+
import { fetchLinodeImages } from './cypress/support/plugins/fetch-linode-images';
1314
import { fetchLinodeRegions } from './cypress/support/plugins/fetch-linode-regions';
1415
import { generateTestWeights } from './cypress/support/plugins/generate-weights';
1516
import { enableHtmlReport } from './cypress/support/plugins/html-report';
@@ -102,6 +103,7 @@ export default defineConfig({
102103
fetchAccount,
103104
fetchLinodeRegions,
104105
fetchLinodeClusters,
106+
fetchLinodeImages,
105107
resetUserPreferences,
106108
regionOverrideCheck,
107109
clusterOverrideCheck,

packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { ui } from 'support/ui';
1212
import { SimpleBackoffMethod } from 'support/util/backoff';
1313
import { cleanUp } from 'support/util/cleanup';
14+
import { chooseImage } from 'support/util/images';
1415
import { createTestLinode } from 'support/util/linodes';
1516
import {
1617
pollImageStatus,
@@ -185,8 +186,9 @@ describe('Create stackscripts', () => {
185186
it('creates a StackScript and deploys a Linode with it', () => {
186187
const stackscriptLabel = randomLabel();
187188
const stackscriptDesc = randomPhrase();
188-
const stackscriptImage = 'Alpine 3.19';
189-
189+
// use random image. can specify image w/ getImageByLabel, then set images option in chooseImage
190+
const randomImage = chooseImage();
191+
const stackscriptImage = randomImage.label;
190192
const linodeLabel = randomLabel();
191193
const linodeRegion = chooseRegion({ capabilities: ['Vlans'] });
192194

packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { ui } from 'support/ui';
1111
import { depaginate } from 'support/util/paginate';
1212
import { randomLabel, randomPhrase } from 'support/util/random';
1313

14+
import { isImageDeprecated } from 'src/components/ImageSelect/utilities';
15+
1416
import type { Image, StackScript } from '@linode/api-v4';
1517

1618
// StackScript fixture paths.
@@ -98,7 +100,7 @@ describe('Update stackscripts', () => {
98100
getImages({ page }, { is_public: true })
99101
);
100102
return allPublicImages.find(
101-
(image) => image.vendor === 'Alpine' && image.deprecated === false
103+
(image) => image.vendor === 'Alpine' && !isImageDeprecated(image)
102104
);
103105
};
104106

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getImages } from '@linode/api-v4';
2+
3+
import type { CypressPlugin } from './plugin';
4+
import type { Image, ResourcePage } from '@linode/api-v4';
5+
6+
/**
7+
* Fetches and stores Linode image data in Cypress environment object.
8+
*/
9+
export const fetchLinodeImages: CypressPlugin = async (_, config) => {
10+
const data: ResourcePage<Image> = await getImages({ page_size: 500 });
11+
12+
const images = data.data;
13+
return {
14+
...config,
15+
env: {
16+
...config.env,
17+
cloudManagerImages: images,
18+
},
19+
};
20+
};
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { randomItem } from 'support/util/random';
2+
3+
import { isImageDeprecated } from 'src/components/ImageSelect/utilities';
4+
5+
import type { Image, ImageCapabilities } from '@linode/api-v4';
6+
/**
7+
* Images that cannot be selected using `chooseImages()`.
8+
*/
9+
const disallowedImageIds: string[] = [];
10+
11+
/**
12+
* All Linode images available to the current Cloud Manager user.
13+
*
14+
* Retrieved via Linode APIv4 during Cypress start-up.
15+
*/
16+
export const images: Image[] = Cypress.env('cloudManagerImages') as Image[];
17+
18+
/**
19+
* Returns a known Cloud Manager image at random, or returns a user-chosen
20+
* image if one was specified.
21+
*
22+
* @param options - Image selection options.
23+
*
24+
* @returns Object describing a Cloud Manager image to use during tests.
25+
*/
26+
export const chooseImage = (options?: ChooseImageOptions): Image => {
27+
return randomItem(resolveSearchImages(options));
28+
};
29+
30+
/**
31+
* Returns an array of Image objects that meet the given criteria.
32+
*
33+
* @param options - Object describing Image selection criteria.
34+
*
35+
* @throws If no images meet the desired criteria.
36+
* @throws If an override image is specified which does not meet the given criteria.
37+
*
38+
* @returns Array of Image objects that meet criteria specified by `options` param.
39+
*/
40+
const resolveSearchImages = (options?: ChooseImageOptions): Image[] => {
41+
const imageFixtures = options?.images ?? images;
42+
const currentImages = imageFixtures.filter(
43+
(image) => !isImageDeprecated(image)
44+
);
45+
const requiredCapabilities = options?.capabilities ?? [];
46+
const allDisallowedImageIds = [
47+
...disallowedImageIds,
48+
...(options?.exclude ?? []),
49+
];
50+
const capableImages = imagesWithCapabilities(
51+
currentImages,
52+
requiredCapabilities
53+
).filter((image: Image) => !allDisallowedImageIds.includes(image.id));
54+
55+
if (!capableImages.length) {
56+
throw new Error(
57+
`No images are available with the required capabilities: ${requiredCapabilities.join(
58+
', '
59+
)}`
60+
);
61+
}
62+
return capableImages;
63+
};
64+
65+
/**
66+
* Returns `true` if the given Image has all of the given capabilities and availability for each capability.
67+
*
68+
* @param image - Image to check capabilities.
69+
* @param capabilities - ImageCapabilities to check.
70+
*
71+
* @returns `true` if `image` has all of the given capabilities.
72+
*/
73+
const imageHasCapabilities = (
74+
image: Image,
75+
capabilities: ImageCapabilities[]
76+
): boolean => {
77+
return capabilities.every((capability) =>
78+
image.capabilities.includes(capability)
79+
);
80+
};
81+
82+
/**
83+
* Returns an array of Image objects that have all of the given capabilities.
84+
*
85+
* @param images - Images from which to search.
86+
* @param capabilities - ImageCapabilities to check.
87+
*
88+
* @returns Array of Image objects containing the required capabilities.
89+
*/
90+
const imagesWithCapabilities = (
91+
images: Image[],
92+
capabilities: ImageCapabilities[]
93+
): Image[] => {
94+
return images.filter((image: Image) =>
95+
imageHasCapabilities(image, capabilities)
96+
);
97+
};
98+
99+
/**
100+
* Returns an object describing a Cloud Manager image with the given label.
101+
*
102+
* If no known image exists with the given human-readable label, an error is
103+
* thrown.
104+
*
105+
* @param label - Label (API or Cloud-specific) of the image to find.
106+
* @param searchImages - Optional array of Images from which to search.
107+
*
108+
* @throws When no image exists in the `images` array with the given label.
109+
*/
110+
export const getImageByLabel = (label: string, searchImages?: Image[]) => {
111+
const image = (searchImages ?? images).find(
112+
(findImage: Image) => findImage.label === label
113+
);
114+
if (!image) {
115+
throw new Error(
116+
`Unable to find image by label. Unknown image label '${label}'.`
117+
);
118+
}
119+
return image;
120+
};
121+
122+
interface ChooseImageOptions {
123+
/**
124+
* If specified, the image returned will support the defined capabilities
125+
* @example ['cloud-init', 'distributed-sites']
126+
*/
127+
capabilities?: ImageCapabilities[];
128+
129+
/**
130+
* Array of image IDs to exclude from results.
131+
*/
132+
exclude?: string[];
133+
134+
/**
135+
* Images from which to choose. If unspecified, Images exposed by the API will be used.
136+
*/
137+
images?: Image[];
138+
}

0 commit comments

Comments
 (0)