Skip to content

Commit 72996fe

Browse files
authored
minor: use pre-stopped mock instance in e2e tests (#3208)
A bunch of tests (11 tests) have to wait for an instance to stop in order to do things you need to stop the instance to do. If the instance is already stopped it saves like 5 seconds per test. The savings will probably be diluted by 5x parallelism, so it may only save 10 seconds on the full run, but it's probably still worth it.
1 parent acf669b commit 72996fe

11 files changed

Lines changed: 128 additions & 74 deletions

mock-api/disk.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Disk, DiskState } from '@oxide/api'
99

1010
import { GiB } from '~/util/units'
1111

12-
import { instance } from './instance'
12+
import { instance, stoppedInstance } from './instance'
1313
import type { Json } from './json-type'
1414
import { Rando } from './msw/rando'
1515
import { project, project2 } from './project'
@@ -83,9 +83,41 @@ export const disk2: Json<Disk> = {
8383
read_only: false,
8484
}
8585

86+
export const stoppedBootDisk: Json<Disk> = {
87+
id: 'f5bc2085-d18e-4698-86ab-69c62a74e541',
88+
name: 'disk-stopped-boot',
89+
description: 'boot disk for db-stopped',
90+
project_id: project.id,
91+
time_created: new Date().toISOString(),
92+
time_modified: new Date().toISOString(),
93+
state: { state: 'attached', instance: stoppedInstance.id },
94+
device_path: '/abc',
95+
size: 2 * GiB,
96+
block_size: 2048,
97+
disk_type: 'distributed',
98+
read_only: false,
99+
}
100+
101+
export const stoppedDataDisk: Json<Disk> = {
102+
id: '8f25d709-a76b-4399-a105-f2cfd8e52604',
103+
name: 'disk-stopped-data',
104+
description: 'data disk for db-stopped',
105+
project_id: project.id,
106+
time_created: new Date().toISOString(),
107+
time_modified: new Date().toISOString(),
108+
state: { state: 'attached', instance: stoppedInstance.id },
109+
device_path: '/def',
110+
size: 4 * GiB,
111+
block_size: 2048,
112+
disk_type: 'distributed',
113+
read_only: false,
114+
}
115+
86116
export const disks: Json<Disk>[] = [
87117
disk1,
88118
disk2,
119+
stoppedBootDisk,
120+
stoppedDataDisk,
89121
{
90122
id: '3b768903-1d0b-4d78-9308-c12d3889bdfb',
91123
name: 'disk-3',

mock-api/instance.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ export const instanceDb2: Json<Instance> = {
130130
boot_disk_id: '48f94570-60d8-401c-857f-5bf912d2d3fc', // disk-2: needs to be written out here to reduce circular dependencies
131131
}
132132

133+
// Pre-stopped instance used by tests that only stop an instance to bypass a
134+
// "must be stopped" precondition. Lets those tests skip the stop dance.
135+
export const stoppedInstance: Json<Instance> = {
136+
...base,
137+
id: '43ad3fc4-cf13-49ae-8171-35dbf0dd30f0',
138+
name: 'db-stopped',
139+
description: 'a stopped instance',
140+
hostname: 'oxide.com',
141+
project_id: project.id,
142+
run_state: 'stopped',
143+
boot_disk_id: 'f5bc2085-d18e-4698-86ab-69c62a74e541', // disk-stopped-boot
144+
}
145+
133146
export const instances: Json<Instance>[] = [
134147
instance,
135148
failedInstance,
@@ -139,4 +152,5 @@ export const instances: Json<Instance>[] = [
139152
failedCooledRestartNever,
140153
instanceUpdateError,
141154
instanceDb2,
155+
stoppedInstance,
142156
]

mock-api/msw/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ const initDb = {
626626
ipPools: [...mock.ipPools],
627627
ipPoolSilos: [...mock.ipPoolSilos],
628628
ipPoolRanges: [...mock.ipPoolRanges],
629-
networkInterfaces: [mock.networkInterface],
629+
networkInterfaces: [mock.networkInterface, mock.stoppedInstanceNic],
630630
physicalDisks: [...mock.physicalDisks],
631631
projects: [...projects],
632632
racks: [...mock.racks],

mock-api/network-interface.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import type { InstanceNetworkInterface } from '@oxide/api'
99

10-
import { instance } from './instance'
10+
import { instance, stoppedInstance } from './instance'
1111
import type { Json } from './json-type'
1212
import { vpc, vpcSubnet } from './vpc'
1313

@@ -36,3 +36,23 @@ export const networkInterface: Json<InstanceNetworkInterface> = {
3636
time_modified: new Date().toISOString(),
3737
vpc_id: vpc.id,
3838
}
39+
40+
export const stoppedInstanceNic: Json<InstanceNetworkInterface> = {
41+
id: '0864924b-17b0-4467-9dd1-f2461bb84b9a',
42+
name: 'my-nic',
43+
description: 'a network interface',
44+
primary: true,
45+
instance_id: stoppedInstance.id,
46+
ip_stack: {
47+
type: 'dual_stack',
48+
value: {
49+
v4: { ip: '172.30.0.11', transit_ips: ['172.30.0.0/22'] },
50+
v6: { ip: '::2', transit_ips: ['::/64'] },
51+
},
52+
},
53+
mac: '',
54+
subnet_id: vpcSubnet.id,
55+
time_created: new Date().toISOString(),
56+
time_modified: new Date().toISOString(),
57+
vpc_id: vpc.id,
58+
}

test/e2e/disks.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ test('List disks and snapshot', async ({ page }) => {
5252
await page.goto('/projects/mock-project/disks')
5353

5454
const table = page.getByRole('table')
55-
await expect(table.getByRole('row')).toHaveCount(14) // 13 + header
55+
await expect(table.getByRole('row')).toHaveCount(16) // 15 + header
5656

5757
// check one attached and one not attached
5858
await expectRowVisible(table, {

test/e2e/firewall-rules.e2e.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,10 +639,17 @@ test('arbitrary values combobox', async ({ page }) => {
639639
'not-there-yet',
640640
'instance-update-error',
641641
'db2',
642+
'db-stopped',
642643
])
643644

644645
await input.fill('d')
645-
await expectOptions(page, ['db1', 'instance-update-error', 'db2', 'Custom: d'])
646+
await expectOptions(page, [
647+
'db1',
648+
'instance-update-error',
649+
'db2',
650+
'db-stopped',
651+
'Custom: d',
652+
])
646653

647654
await input.blur()
648655
await expect(page.getByRole('option')).toBeHidden()
@@ -651,7 +658,13 @@ test('arbitrary values combobox', async ({ page }) => {
651658
await input.focus()
652659

653660
// same options show up after blur (there was a bug around this)
654-
await expectOptions(page, ['db1', 'instance-update-error', 'db2', 'Custom: d'])
661+
await expectOptions(page, [
662+
'db1',
663+
'instance-update-error',
664+
'db2',
665+
'db-stopped',
666+
'Custom: d',
667+
])
655668

656669
// make sure typing in ICMP filter input actually updates the underlying value,
657670
// triggering a validation error for bad input. without onInputChange binding

test/e2e/instance-disks.e2e.ts

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,7 @@ test('Disabled actions', async ({ page }) => {
8686
})
8787

8888
test('Attach disk', async ({ page }) => {
89-
await page.goto('/projects/mock-project/instances/db1')
90-
91-
// Have to stop instance to edit disks
92-
await stopInstance(page)
89+
await page.goto('/projects/mock-project/instances/db-stopped')
9390

9491
// Attach existing disk form
9592
await page.click('role=button[name="Attach existing disk"]')
@@ -100,8 +97,8 @@ test('Attach disk', async ({ page }) => {
10097
await expectVisible(page, ['role=dialog >> text="Disk name is required"'])
10198

10299
await page.getByRole('combobox', { name: 'Disk name' }).click()
103-
// disk-1 is already attached, so should not be visible in the list
104-
await expectNotVisible(page, ['role=option[name="disk-1"]'])
100+
// disk-stopped-boot is already attached, so should not be visible in the list
101+
await expectNotVisible(page, ['role=option[name="disk-stopped-boot"]'])
105102
await expectVisible(page, ['role=option[name="disk-3"]', 'role=option[name="disk-4"]'])
106103
await page.click('role=option[name="disk-3"]')
107104

@@ -110,14 +107,11 @@ test('Attach disk', async ({ page }) => {
110107
})
111108

112109
test('Create disk', async ({ page }) => {
113-
await page.goto('/projects/mock-project/instances/db1')
110+
await page.goto('/projects/mock-project/instances/db-stopped')
114111

115112
const row = page.getByRole('cell', { name: 'created-disk' })
116113
await expect(row).toBeHidden()
117114

118-
// Have to stop instance to edit disks
119-
await stopInstance(page)
120-
121115
// New disk form
122116
const createForm = page.getByRole('dialog', { name: 'Create disk' })
123117
await expect(createForm).toBeHidden()
@@ -138,17 +132,14 @@ test('Create disk', async ({ page }) => {
138132
})
139133

140134
test('Detach disk', async ({ page }) => {
141-
await page.goto('/projects/mock-project/instances/db1')
135+
await page.goto('/projects/mock-project/instances/db-stopped')
142136

143-
// Have to stop instance to edit disks
144-
await stopInstance(page)
145-
146-
const successMsg = page.getByText('Disk disk-2 detached').first()
147-
const row = page.getByRole('row', { name: 'disk-2' })
137+
const successMsg = page.getByText('Disk disk-stopped-data detached').first()
138+
const row = page.getByRole('row', { name: 'disk-stopped-data' })
148139
await expect(row).toBeVisible()
149140
await expect(successMsg).toBeHidden()
150141

151-
await clickRowAction(page, 'disk-2', 'Detach')
142+
await clickRowAction(page, 'disk-stopped-data', 'Detach')
152143
await page.getByRole('button', { name: 'Confirm' }).click()
153144
await expect(successMsg).toBeVisible()
154145
await expect(row).toBeHidden() // disk row goes away
@@ -176,9 +167,7 @@ test('Snapshot disk', async ({ page }) => {
176167
})
177168

178169
test('Attach disk error clears when modal closes', async ({ page }) => {
179-
await page.goto('/projects/mock-project/instances/db1')
180-
181-
await stopInstance(page)
170+
await page.goto('/projects/mock-project/instances/db-stopped')
182171

183172
// Attach disks until we hit the limit
184173
const disksToAttach = [
@@ -244,62 +233,59 @@ test('Attach disk error clears when modal closes', async ({ page }) => {
244233
})
245234

246235
test('Change boot disk', async ({ page }) => {
247-
await page.goto('/projects/mock-project/instances/db1')
236+
await page.goto('/projects/mock-project/instances/db-stopped')
248237

249-
// assert disk-1 is boot disk, disk-2 also there
250238
const bootDiskTable = page.getByRole('table', { name: 'Boot disk' })
251239
const otherDisksTable = page.getByRole('table', { name: 'Additional disks' })
252240
const confirm = page.getByRole('button', { name: 'Confirm' })
253241
const noBootDisk = page.getByText('No boot disk set')
254242
const noOtherDisks = page.getByText('No other disks')
255243

256-
const disk1 = { Disk: 'disk-1', size: '2 GiB' }
257-
const disk2 = { Disk: 'disk-2', size: '4 GiB' }
258-
259-
await expectRowVisible(bootDiskTable, disk1)
260-
await expectRowVisible(otherDisksTable, disk2)
244+
const bootDisk = { Disk: 'disk-stopped-boot', size: '2 GiB' }
245+
const dataDisk = { Disk: 'disk-stopped-data', size: '4 GiB' }
261246

262-
await stopInstance(page)
247+
await expectRowVisible(bootDiskTable, bootDisk)
248+
await expectRowVisible(otherDisksTable, dataDisk)
263249

264-
// Set disk-2 as boot disk
265-
await clickRowAction(page, 'disk-2', 'Set as boot disk')
250+
// Set disk-stopped-data as boot disk
251+
await clickRowAction(page, 'disk-stopped-data', 'Set as boot disk')
266252
await confirm.click()
267253

268-
await expectRowVisible(bootDiskTable, disk2)
269-
await expectRowVisible(otherDisksTable, disk1)
254+
await expectRowVisible(bootDiskTable, dataDisk)
255+
await expectRowVisible(otherDisksTable, bootDisk)
270256

271257
// Unset boot disk
272258
await expect(noBootDisk).toBeHidden()
273259

274-
await clickRowAction(page, 'disk-2', 'Unset as boot disk')
260+
await clickRowAction(page, 'disk-stopped-data', 'Unset as boot disk')
275261
await confirm.click()
276262

277263
await expect(noBootDisk).toBeVisible()
278-
await expectRowVisible(otherDisksTable, disk1)
279-
await expectRowVisible(otherDisksTable, disk2)
264+
await expectRowVisible(otherDisksTable, bootDisk)
265+
await expectRowVisible(otherDisksTable, dataDisk)
280266

281267
await expect(page.getByText('Setting a boot disk is recommended')).toBeVisible()
282268

283269
// detach disk so there's only one
284-
await clickRowAction(page, 'disk-2', 'Detach')
270+
await clickRowAction(page, 'disk-stopped-data', 'Detach')
285271
await page.getByRole('button', { name: 'Confirm' }).click()
286272

287-
await expect(page.getByText('Instance will boot from disk-1')).toBeVisible()
273+
await expect(page.getByText('Instance will boot from disk-stopped-boot')).toBeVisible()
288274

289-
// set disk-1 back as boot disk
290-
await clickRowAction(page, 'disk-1', 'Set as boot disk')
275+
// set disk-stopped-boot back as boot disk
276+
await clickRowAction(page, 'disk-stopped-boot', 'Set as boot disk')
291277
await confirm.click()
292278

293279
await expect(noBootDisk).toBeHidden()
294280
await expect(noOtherDisks).toBeVisible()
295281

296-
// Remove disk-1 altogether, no disks left
297-
await clickRowAction(page, 'disk-1', 'Unset as boot disk')
282+
// Remove disk-stopped-boot altogether, no disks left
283+
await clickRowAction(page, 'disk-stopped-boot', 'Unset as boot disk')
298284
await confirm.click()
299285

300-
await expectRowVisible(otherDisksTable, disk1)
286+
await expectRowVisible(otherDisksTable, bootDisk)
301287

302-
await clickRowAction(page, 'disk-1', 'Detach')
288+
await clickRowAction(page, 'disk-stopped-boot', 'Detach')
303289
await page.getByRole('button', { name: 'Confirm' }).click()
304290

305291
await expect(noBootDisk).toBeVisible()

test/e2e/instance-networking.e2e.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,8 @@ test('Instance networking tab — SNAT IPs', async ({ page }) => {
316316
})
317317

318318
test('Edit network interface - Transit IPs', async ({ page }) => {
319-
await page.goto('/projects/mock-project/instances/db1/networking')
320-
321-
// Stop the instance to enable editing
322-
await stopInstance(page)
319+
// use a stopped instance so editing is enabled
320+
await page.goto('/projects/mock-project/instances/db-stopped/networking')
323321

324322
await clickRowAction(page, 'my-nic', 'Edit')
325323

test/e2e/instance.e2e.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,9 @@ test("polling doesn't close row actions: instances", async ({ page }) => {
314314
await closeToast(page)
315315

316316
const menu = page.getByRole('menu')
317-
const stopped = page.getByText('stopped')
317+
// scope to db1's row — db-stopped is also in the table with state 'stopped'
318+
const db1Row = page.getByRole('row', { name: 'db1', exact: false })
319+
const stopped = db1Row.getByText('stopped')
318320

319321
await expect(menu).toBeHidden()
320322
await expect(stopped).toBeHidden()

test/e2e/network-interface-create.e2e.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
*/
88
import { test } from '@playwright/test'
99

10-
import { expect, expectRowVisible, stopInstance } from './utils'
10+
import { expect, expectRowVisible } from './utils'
1111

1212
test('can create a NIC with a specified IP address', async ({ page }) => {
13-
// go to an instance's Network Interfaces page
14-
await page.goto('/projects/mock-project/instances/db1/networking')
15-
16-
await stopInstance(page)
13+
// use a stopped instance so we can edit NICs
14+
await page.goto('/projects/mock-project/instances/db-stopped/networking')
1715

1816
// open the add network interface side modal
1917
await page.getByRole('button', { name: 'Add network interface' }).click()
@@ -40,10 +38,7 @@ test('can create a NIC with a specified IP address', async ({ page }) => {
4038
})
4139

4240
test('can create a NIC with a blank IP address', async ({ page }) => {
43-
// go to an instance's Network Interfaces page
44-
await page.goto('/projects/mock-project/instances/db1/networking')
45-
46-
await stopInstance(page)
41+
await page.goto('/projects/mock-project/instances/db-stopped/networking')
4742

4843
// open the add network interface side modal
4944
await page.getByRole('button', { name: 'Add network interface' }).click()
@@ -83,9 +78,7 @@ test('can create a NIC with a blank IP address', async ({ page }) => {
8378
})
8479

8580
test('can create a NIC with IPv6 only', async ({ page }) => {
86-
await page.goto('/projects/mock-project/instances/db1/networking')
87-
88-
await stopInstance(page)
81+
await page.goto('/projects/mock-project/instances/db-stopped/networking')
8982

9083
await page.getByRole('button', { name: 'Add network interface' }).click()
9184

@@ -108,9 +101,7 @@ test('can create a NIC with IPv6 only', async ({ page }) => {
108101
})
109102

110103
test('can create a NIC with dual-stack and explicit IPs', async ({ page }) => {
111-
await page.goto('/projects/mock-project/instances/db1/networking')
112-
113-
await stopInstance(page)
104+
await page.goto('/projects/mock-project/instances/db-stopped/networking')
114105

115106
await page.getByRole('button', { name: 'Add network interface' }).click()
116107

0 commit comments

Comments
 (0)