Skip to content

Commit 11126f0

Browse files
committed
Improved fake job server to handle concurrent jobs
1 parent f56f03b commit 11126f0

File tree

6 files changed

+135
-53
lines changed

6 files changed

+135
-53
lines changed

tests/fake-job-server.js

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ const PORT = process.env.PORT || 8080;
44

55
const server = createServer();
66

7-
const tasksStatusMap = new Map();
7+
/** @type {Map<number, string>} */
8+
const jobsStatusMapV1 = new Map();
9+
/** @type {Map<number, string>} */
10+
const jobsStatusMapV2 = new Map();
811

912
server.on('request', (request, response) => {
1013
response.setHeader('Content-type', 'text/plain');
@@ -20,46 +23,65 @@ server.on('request', (request, response) => {
2023
if (!task) {
2124
return badRequest(response, 'missing task identifier');
2225
}
23-
const status = getTaskStatus(task);
26+
const { version, jobId } = extractJobInfo(task);
27+
const status = getJobStatus(version, jobId);
2428
if (request.method === 'GET' && status !== 'submitted') {
25-
tasksStatusMap.delete(task);
29+
console.log(`Removing job ${jobId}`);
30+
getJobStatusMap(version).delete(jobId);
2631
}
2732
response.end(status);
2833
} else {
2934
// PUT method is called by Playwright tests to update the desired task status
30-
if (url.pathname.startsWith('/done')) {
31-
setRunningTasksTo('done');
32-
response.end('done');
33-
} else if (url.pathname.startsWith('/failed')) {
34-
setRunningTasksTo('failed');
35-
response.end('failed');
35+
const match = url.pathname.match(/\/(v1|v2)\/(\d+)/);
36+
if (!match) {
37+
return badRequest(response, `missing job identifier in ${url.pathname}`);
3638
}
39+
const jobId = Number(match[2]);
40+
const status = url.searchParams.get('status');
41+
if (!status) {
42+
return badRequest(response, 'missing status');
43+
}
44+
const version = url.pathname.startsWith('/v2') ? 'v2' : 'v1';
45+
console.log(`Setting job ${jobId} status to ${status}`);
46+
getJobStatusMap(version).set(Number(jobId), status);
47+
response.end(status);
3748
}
3849
});
3950

40-
function getTaskStatus(task) {
41-
if (!tasksStatusMap.has(task)) {
42-
console.log(`Creating entry for task ${task}`);
43-
// Set all the other running tasks to failed
44-
// If there are no flacky tests there shouldn't be other running tasks
45-
setRunningTasksTo('failed');
46-
// Add new task as submitted
47-
tasksStatusMap.set(task, 'submitted');
51+
/**
52+
* @param {string} taskFolder
53+
*/
54+
function extractJobInfo(taskFolder) {
55+
const match = taskFolder.match(/job_(\d+)/);
56+
if (!match) {
57+
throw new Error(`Unable to extract job id from ${taskFolder}`);
4858
}
49-
return tasksStatusMap.get(task);
59+
const version = taskFolder.includes('proj_v2') ? 'v2' : 'v1';
60+
return { jobId: Number(match[1]), version };
5061
}
5162

5263
/**
53-
* We handle only one running task, to simplify the fake server.
54-
* In case of flacky Playwright tests there could be more than one submitted task.
55-
* In that case we set all the submitted task to the same desired final value.
56-
* @param {string} status
64+
* @param {string} version
65+
* @param {number} jobId
66+
* @returns {string|undefined}
5767
*/
58-
function setRunningTasksTo(status) {
59-
for (const [key, value] of tasksStatusMap.entries()) {
60-
if (value === 'submitted') {
61-
tasksStatusMap.set(key, status);
62-
}
68+
function getJobStatus(version, jobId) {
69+
const jobsStatusMap = getJobStatusMap(version);
70+
if (!jobsStatusMap.has(jobId)) {
71+
console.log(`Creating entry for job ${jobId}`);
72+
jobsStatusMap.set(jobId, 'submitted');
73+
}
74+
return jobsStatusMap.get(jobId);
75+
}
76+
77+
/**
78+
* @param {string} version
79+
*/
80+
function getJobStatusMap(version) {
81+
if (version === 'v1') {
82+
return jobsStatusMapV1;
83+
} else {
84+
return jobsStatusMapV2;
6385
}
6486
}
6587

tests/v1/admin_jobs.spec.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitPageLoading } from '../utils.js';
2+
import { waitModalClosed, waitPageLoading } from '../utils.js';
33
import { PageWithWorkflow } from './workflow_fixture.js';
44
import * as fs from 'fs';
55

66
test('Execute a job and show it on the job tables [v1]', async ({ page, request }) => {
77
/** @type {PageWithWorkflow} */
88
let workflow1;
99
await test.step('Create first job and wait its failure', async () => {
10-
workflow1 = await createJob(page, request, 'input', 'output');
11-
await workflow1.triggerTaskFailure();
10+
const job = await createJob(page, request, 'input', 'output');
11+
workflow1 = job.workflow;
12+
await workflow1.triggerTaskFailure(job.jobId);
1213
const jobBadge = page.locator('.badge.text-bg-danger');
1314
await jobBadge.waitFor();
1415
expect(await jobBadge.innerText()).toEqual('failed');
@@ -17,8 +18,12 @@ test('Execute a job and show it on the job tables [v1]', async ({ page, request
1718

1819
/** @type {PageWithWorkflow} */
1920
let workflow2;
21+
/** @type {number} */
22+
let jobId2;
2023
await test.step('Create second job', async () => {
21-
workflow2 = await createJob(page, request, 'input2', 'output2');
24+
const job = await createJob(page, request, 'input2', 'output2');
25+
workflow2 = job.workflow;
26+
jobId2 = job.jobId;
2227
});
2328

2429
await test.step('Open the admin jobs', async () => {
@@ -84,7 +89,7 @@ test('Execute a job and show it on the job tables [v1]', async ({ page, request
8489
});
8590

8691
await test.step('Wait job completion', async () => {
87-
await workflow2.triggerTaskSuccess();
92+
await workflow2.triggerTaskSuccess(jobId2);
8893
const jobBadge = page.locator('.badge.text-bg-success');
8994
await jobBadge.waitFor();
9095
expect(await jobBadge.innerText()).toEqual('done');
@@ -161,19 +166,22 @@ async function getWorkflowRow(page, workflowName) {
161166
* @param {import('@playwright/test').APIRequestContext} request
162167
* @param {string} inputDataset
163168
* @param {string} outputDataset
169+
* @returns {Promise<{ workflow: PageWithWorkflow, jobId: number }>}
164170
*/
165171
async function createJob(page, request, inputDataset, outputDataset) {
166172
const workflow = new PageWithWorkflow(page, request);
173+
/** @type {number|undefined} */
174+
let jobId;
167175
await test.step('Create workflow', async () => {
168176
await workflow.createProject();
169177
await workflow.createDataset(inputDataset, 'image');
170178
await workflow.createDataset(outputDataset, 'zarr');
171179
await workflow.createWorkflow();
172180
await page.waitForURL(/** @type {string} */ (workflow.url));
173181
await addTaskToWorkflow(workflow);
174-
await runWorkflow(page, inputDataset, outputDataset);
182+
jobId = await runWorkflow(page, inputDataset, outputDataset);
175183
});
176-
return workflow;
184+
return { workflow, jobId: /** @type {number} */ (jobId) };
177185
}
178186

179187
/**
@@ -189,8 +197,11 @@ async function addTaskToWorkflow(workflow) {
189197
* @param {import('@playwright/test').Page} page
190198
* @param {string} inputDataset
191199
* @param {string} outputDataset
200+
* @returns {Promise<number>} the id of the job
192201
*/
193202
async function runWorkflow(page, inputDataset, outputDataset) {
203+
/** @type {number|undefined} */
204+
let jobId;
194205
await test.step('Run workflow', async () => {
195206
const runWorkflowBtn = page.getByRole('button', { name: 'Run workflow' });
196207
await runWorkflowBtn.click();
@@ -204,5 +215,16 @@ async function runWorkflow(page, inputDataset, outputDataset) {
204215
const confirmBtn = page.locator('.modal.show').getByRole('button', { name: 'Confirm' });
205216
await confirmBtn.click();
206217
await page.waitForURL(new RegExp(`/v1/projects/(\\d+)/workflows/(\\d+)/jobs`));
218+
await page
219+
.getByRole('row', { name: 'submitted' })
220+
.getByRole('button', { name: 'Info' })
221+
.click();
222+
const modal = page.locator('.modal.show');
223+
await modal.waitFor();
224+
const value = await modal.getByRole('listitem').nth(1).textContent();
225+
jobId = Number(value?.trim());
226+
await modal.getByRole('button', { name: 'Close' }).click();
227+
await waitModalClosed(page);
207228
});
229+
return /** @type {number} */ (jobId);
208230
}

tests/v1/jobs.spec.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ test('Execute jobs', async ({ page, workflow }) => {
2525
});
2626

2727
await test.step('Check workflow jobs page', async () => {
28-
await page.waitForURL(`/v1/projects/${workflow.projectId}/workflows/${workflow.workflowId}/jobs`);
28+
await page.waitForURL(
29+
`/v1/projects/${workflow.projectId}/workflows/${workflow.workflowId}/jobs`
30+
);
2931
await page.locator('table tbody').waitFor();
3032
expect(await page.locator('table tbody tr').count()).toEqual(1);
3133
const cells = await page.locator('table tbody tr td').allInnerTexts();
@@ -82,7 +84,7 @@ test('Execute jobs', async ({ page, workflow }) => {
8284
const modalTitle = page.locator('.modal.show .modal-title');
8385
await modalTitle.waitFor();
8486
await expect(modalTitle).toHaveText('Workflow Job logs');
85-
await workflow.triggerTaskFailure();
87+
await workflow.triggerTaskFailure(jobId);
8688
await page.waitForFunction(() => {
8789
const modalBody = document.querySelector('.modal.show .modal-body');
8890
return modalBody instanceof HTMLElement && modalBody.innerText.includes('TASK ERROR');

tests/v1/workflow_fixture.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,26 @@ export class PageWithWorkflow extends PageWithProject {
7171
await this.addUserTask('Fake Task');
7272
}
7373

74-
async triggerTaskSuccess() {
75-
await this.setTaskStatus('done');
74+
/**
75+
* @param {number} jobId
76+
*/
77+
async triggerTaskSuccess(jobId) {
78+
await this.setTaskStatus(jobId, 'done');
7679
}
7780

78-
async triggerTaskFailure() {
79-
await this.setTaskStatus('failed');
81+
/**
82+
* @param {number} jobId
83+
*/
84+
async triggerTaskFailure(jobId) {
85+
await this.setTaskStatus(jobId, 'failed');
8086
}
8187

8288
/**
89+
* @param {number} jobId
8390
* @param {import('$lib/types.js').JobStatus} status
8491
*/
85-
async setTaskStatus(status) {
86-
const response = await this.request.put(`http://localhost:8080/${status}`);
92+
async setTaskStatus(jobId, status) {
93+
const response = await this.request.put(`http://localhost:8080/v1/${jobId}?status=${status}`);
8794
expect(response.ok()).toEqual(true);
8895
}
8996

tests/v2/admin_jobs.spec.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitPageLoading } from '../utils.js';
2+
import { waitModalClosed, waitPageLoading } from '../utils.js';
33
import { PageWithWorkflow } from './workflow_fixture.js';
44
import * as fs from 'fs';
55
import { createDataset } from './dataset_utils.js';
@@ -37,12 +37,15 @@ test('Execute a job and show it on the job tables [v2]', async ({ page, request
3737
let workflow2;
3838
/** @type {string} */
3939
let dataset2;
40+
/** @type {number} */
41+
let jobId2;
4042
await test.step('Create second job', async () => {
4143
const job = await createJob(page, request, async function (workflow) {
4244
await workflow.addUserTask(taskName);
4345
});
4446
workflow2 = job.workflow;
4547
dataset2 = job.dataset;
48+
jobId2 = job.jobId;
4649
await waitTaskSubmitted(page, 1);
4750
});
4851

@@ -105,7 +108,7 @@ test('Execute a job and show it on the job tables [v2]', async ({ page, request
105108
});
106109

107110
await test.step('Wait job completion', async () => {
108-
await workflow2.triggerTaskSuccess();
111+
await workflow2.triggerTaskSuccess(jobId2);
109112
const jobBadge = page.locator('.badge.text-bg-success');
110113
await jobBadge.waitFor();
111114
expect(await jobBadge.innerText()).toEqual('done');
@@ -179,28 +182,33 @@ async function getWorkflowRow(page, workflowName) {
179182
* @param {import('@playwright/test').Page} page
180183
* @param {import('@playwright/test').APIRequestContext} request
181184
* @param {(workflow: PageWithWorkflow) => Promise<void>} addTask
182-
* @returns {Promise<{ workflow: PageWithWorkflow, dataset: string }>}
185+
* @returns {Promise<{ workflow: PageWithWorkflow, dataset: string, jobId: number }>}
183186
*/
184187
async function createJob(page, request, addTask) {
185188
const workflow = new PageWithWorkflow(page, request);
186189
let datasetName = '';
190+
/** @type {number|undefined} */
191+
let jobId;
187192
await test.step('Create workflow', async () => {
188193
const projectId = await workflow.createProject();
189194
const datasetResult = await createDataset(page, projectId);
190195
datasetName = datasetResult.name;
191196
await workflow.createWorkflow();
192197
await page.waitForURL(/** @type {string} */ (workflow.url));
193198
await addTask(workflow);
194-
await runWorkflow(page, datasetName);
199+
jobId = await runWorkflow(page, datasetName);
195200
});
196-
return { workflow, dataset: datasetName };
201+
return { workflow, dataset: datasetName, jobId: /** @type {number} */ (jobId) };
197202
}
198203

199204
/**
200205
* @param {import('@playwright/test').Page} page
201206
* @param {string} dataset
207+
* @returns {Promise<number>} the id of the job
202208
*/
203209
async function runWorkflow(page, dataset) {
210+
/** @type {number|undefined} */
211+
let jobId;
204212
await test.step('Run workflow', async () => {
205213
await page
206214
.getByRole('combobox', { name: 'Dataset', exact: true })
@@ -215,5 +223,19 @@ async function runWorkflow(page, dataset) {
215223
await runBtn.click();
216224
const confirmBtn = page.locator('.modal.show').getByRole('button', { name: 'Confirm' });
217225
await confirmBtn.click();
226+
await page.getByRole('link', { name: 'List jobs' }).click();
227+
await waitPageLoading(page);
228+
await page
229+
.getByRole('row', { name: 'submitted' })
230+
.getByRole('button', { name: 'Info' })
231+
.click();
232+
const modal = page.locator('.modal.show');
233+
await modal.waitFor();
234+
const value = await modal.getByRole('listitem').nth(1).textContent();
235+
jobId = Number(value?.trim());
236+
await modal.getByRole('button', { name: 'Close' }).click();
237+
await waitModalClosed(page);
238+
await page.goBack();
218239
});
240+
return /** @type {number} */ (jobId);
219241
}

tests/v2/workflow_fixture.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,26 @@ export class PageWithWorkflow extends PageWithProject {
5454
await this.addUserTask('Fake Task');
5555
}
5656

57-
async triggerTaskSuccess() {
58-
await this.setTaskStatus('done');
57+
/**
58+
* @param {number} jobId
59+
*/
60+
async triggerTaskSuccess(jobId) {
61+
await this.setTaskStatus(jobId, 'done');
5962
}
6063

61-
async triggerTaskFailure() {
62-
await this.setTaskStatus('failed');
64+
/**
65+
* @param {number} jobId
66+
*/
67+
async triggerTaskFailure(jobId) {
68+
await this.setTaskStatus(jobId, 'failed');
6369
}
6470

6571
/**
72+
* @param {number} jobId
6673
* @param {import('$lib/types.js').JobStatus} status
6774
*/
68-
async setTaskStatus(status) {
69-
const response = await this.request.put(`http://localhost:8080/${status}`);
75+
async setTaskStatus(jobId, status) {
76+
const response = await this.request.put(`http://localhost:8080/v2/${jobId}?status=${status}`);
7077
expect(response.ok()).toEqual(true);
7178
}
7279

0 commit comments

Comments
 (0)