Skip to content

Commit

Permalink
implementation of scenario-user-configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
hoeppner-dataport committed Jul 19, 2024
1 parent 02db6d0 commit 080d5bc
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 145 deletions.
272 changes: 144 additions & 128 deletions apps/server/src/modules/board/loadtest/board-collaboration.load.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { InputFormat } from '@shared/domain/types';
import { ColumnResponse, LinkContentBody, RichTextContentBody } from '../controller/dto';
/* eslint-disable no-await-in-loop */
/* eslint-disable no-promise-executor-return */
/* eslint-disable no-process-env */
import { ColumnResponse, LinkContentBody } from '../controller/dto';
import { createLoadtestClient } from './loadtestClientFactory';

// const TARGET = 'https://main.dbc.dbildungscloud.dev';
const TARGET = 'http://localhost:4450';
type UrlConfiguration = {
websocket: string;
api: string;
web: string;
};

type UserProfile = {
name: string;
sleepMs: number;
maxCards: number;
};

const fastEditor: UserProfile = { name: 'fastEditor', sleepMs: 1000, maxCards: 10 };
const slowEditor: UserProfile = { name: 'slowEditor', sleepMs: 3000, maxCards: 5 };
const viewer: UserProfile = { name: 'viewer', sleepMs: 1000, maxCards: 0 };

const addUsers = (userProfile: UserProfile, count: number) => Array(count).fill(userProfile) as Array<UserProfile>;

const viewersClass = { name: 'viewersClass', users: [...addUsers(fastEditor, 1), ...addUsers(viewer, 30)] };
const collaborativeClass = { name: 'collaborativeClass', users: [...addUsers(slowEditor, 30)] };

describe('Board Collaboration Load Test', () => {
const getToken = (): string => {
Expand All @@ -15,74 +35,59 @@ describe('Board Collaboration Load Test', () => {
return token;
};

// const configureSocket = (ctx: {}) => {
// if (ctx.vars.target === undefined) {
// // eslint-disable-next-line no-process-env
// ctx.vars.target = process.env.target ? process.env.target : 'http://localhost:4450';
// }

// ctx.vars.target = ctx.vars.target.replace(/\/$/, '');
// ctx.vars.api = ctx.vars.target;
// ctx.vars.web = ctx.vars.target;
// if (/localhost/i.test(ctx.vars.target)) {
// ctx.vars.api = ctx.vars.target.replace('4450', '3030');
// ctx.vars.web = ctx.vars.target.replace('4450', '4000');
// }
// };

// const selectCourse = async (ctx) => {
// const response = await fetch(`${ctx.vars.api}/api/v3/courses`, {
// method: "GET",
// credentials: "include",
// headers: {
// "Content-Type": "application/json",
// accept: "application/json",
// Authorization: `Bearer ${getToken()}`,
// },
// });
// const body = await response.json();
// if (body.data.length === 0) {
// throw new Error("No courses found for user");
// }
// const course = body.data[0];
// return course;
// };

// const createBoard = async (ctx, course) => {
// console.log(course.title);
// const boardTitle = `${new Date()
// .toISOString()
// .replace(/T/, " ")
// .replace(/(\d\d:\d\d).*$/, "$1")} - Lasttest`;
// const response = await fetch(`${ctx.vars.api}/api/v3/boards`, {
// method: "POST",
// credentials: "include",
// headers: {
// "Content-Type": "application/json",
// accept: "application/json",
// Authorization: `Bearer ${getToken()}`,
// },
// body: JSON.stringify({
// title: boardTitle,
// parentId: course.id,
// parentType: "course",
// layout: "columns",
// }),
// });
// const body = await response.json();
// ctx.vars.board_id = body.id;
// };
const getUrlConfiguration = (): UrlConfiguration => {
const { target } = process.env;
if (target === undefined || /localhost/.test(target)) {
return {
websocket: 'http://localhost:4450',
api: 'http://localhost:3030',
web: 'http://localhost:4000',
};
}
return {
websocket: target,
api: target,
web: target,
};
};

const createBoard = async (apiBaseUrl: string, courseId: string) => {
const boardTitle = `${new Date()
.toISOString()
.replace(/T/, ' ')
.replace(/(\d\d:\d\d).*$/, '$1')} - Lasttest`;

const response = await fetch(`${apiBaseUrl}/api/v3/boards`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
accept: 'application/json',
Authorization: `Bearer ${getToken()}`,
},
body: JSON.stringify({
title: boardTitle,
parentId: courseId,
parentType: 'course',
layout: 'columns',
}),
});
const body = (await response.json()) as unknown as { id: string };
return body.id;
};

const createBoards = async (apiBaseUrl: string, courseId: string, amount: number) => {
const promises = Array(amount)
.fill(1)
.map(() => createBoard(apiBaseUrl, courseId));
const results = await Promise.all(promises);
return results;
};

async function sleep(ms: number) {
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => setTimeout(resolve, ms));
}

// async function showBoardlink(ctx) {
// console.log(`${ctx.vars.web}/rooms/${ctx.vars.board_id}/board\n\n`);
// await sleep(1000);
// }

const randomLink = (): LinkContentBody => {
const links = [
{ url: 'https://www.google.com', title: 'Google' },
Expand All @@ -106,93 +111,104 @@ describe('Board Collaboration Load Test', () => {
return texts[Math.floor(Math.random() * texts.length)];
};

const randomCardTitle = (): string => {
const titles = ['Sonstiges', 'Einleitung', 'Beispiele', 'Geschichte', 'Meeresbewohner'];
return titles[Math.floor(Math.random() * titles.length)];
};

let columnCount = 0;

const createColumnContent = async (socket: ReturnType<typeof createLoadtestClient>, column: ColumnResponse) => {
const createRandomCard = async (
socket: ReturnType<typeof createLoadtestClient>,
column: ColumnResponse,
pauseMs = 1000
) => {
const card = await socket.createCard({ columnId: column.id });
await socket.fetchCard({ cardIds: [card.id] });
await sleep(1000);
await socket.updateCardTitle({ cardId: card.id, newTitle: 'Hello World' });
await sleep(1000);
await socket.createAndUpdateLinkElement(card.id, randomLink());
await sleep(1000);
const card2 = await socket.createCard({ columnId: column.id });
await socket.fetchCard({ cardIds: [card2.id] });

await socket.updateCardTitle({ cardId: card2.id, newTitle: 'Something' });
await sleep(1000);
await sleep(1000);
await socket.createAndUpdateTextElement(card2.id, randomRichContentBody());
await sleep(1000);
await socket.createAndUpdateTextElement(card2.id, randomRichContentBody());
await sleep(1000);
await socket.createAndUpdateTextElement(card2.id, randomRichContentBody());
await sleep(1000);

// if (Math.random() > 0.9) {
// const from = Math.floor(Math.random() * columnCount);
// const to = Math.floor(Math.random() * columnCount);
// if (from !== to) {
// await socket.moveColumn(column.id, from, to);
// }
// }
await sleep(pauseMs);
await socket.updateCardTitle({ cardId: card.id, newTitle: randomCardTitle() });
await sleep(pauseMs);
for (let i = 0; i < Math.floor(Math.random() * 5); i += 1) {
if (Math.random() > 0.5) {
await socket.createAndUpdateLinkElement(card.id, randomLink());
} else {
await socket.createAndUpdateTextElement(card.id, randomRichContentBody());
}
await sleep(pauseMs);
}
};

const paintAColumn = async (target: string, boardId: string) => {
const createEditor = async (target: string, boardId: string, userProfile: UserProfile) => {
const socket = createLoadtestClient(target, boardId, getToken());
const column = await socket.createColumn();
columnCount += 1;
const columnNumber = columnCount;
await socket.updateColumnTitle({ columnId: column.id, newTitle: `${columnNumber}. Spalte` });
for (let i = 0; i < 3; i += 1) {
// eslint-disable-next-line no-await-in-loop
await createColumnContent(socket, column);
if (userProfile.maxCards > 0) {
const column = await socket.createColumn();
columnCount += 1;
const columnNumber = columnCount;
await socket.updateColumnTitle({ columnId: column.id, newTitle: `${columnNumber}. Spalte` });

for (let i = 0; i < userProfile.maxCards; i += 1) {
await createRandomCard(socket, column, userProfile.sleepMs);
}
await socket.updateColumnTitle({ columnId: column.id, newTitle: `${columnNumber}. Spalte (fertig)` });

const summaryCard = await socket.createCard({ columnId: column.id });
// await socket.moveCard(column.id, summaryCard.id, socket.getCardPosition(summaryCard.id), 0);
const responseTimes = socket.getResponseTimes();
const sum = responseTimes.reduce((all, cur) => all + cur.responseTime, 0);
await socket.updateCardTitle({
cardId: summaryCard.id,
newTitle: `${responseTimes.length} requests (avg response time: ${(sum / responseTimes.length).toFixed(2)} ms)`,
});
}
await socket.updateColumnTitle({ columnId: column.id, newTitle: `${columnNumber}. Spalte (fertig)` });
const summaryCard = await socket.createCard({ columnId: column.id });
// await socket.moveCard(column.id, summaryCard.id, socket.getCardPosition(summaryCard.id), 0);
const responseTimes = socket.getResponseTimes();
const sum = responseTimes.reduce((all, cur) => all + cur.responseTime, 0);
await socket.updateCardTitle({
cardId: summaryCard.id,
newTitle: `${responseTimes.length} requests (avg response time: ${(sum / responseTimes.length).toFixed(2)} ms)`,
});
};

const setup = async (target: string, clientCount: number) => {
// configureSocket(ctx);
// const course = await selectCourse(ctx);
// await createBoard(ctx, course);
const boardId = '6698fa3b097743c2498aa813';
// await showBoardlink(ctx);
const runBoardTest = async (boardId: string, configuration: { users: UserProfile[] }, urls: UrlConfiguration) => {
const fastEditorPromises = configuration.users.map((userProfile: UserProfile) =>
createEditor(urls.websocket, boardId, userProfile)
);

const socket = createLoadtestClient(target, boardId, getToken());
await sleep(2000);
await socket.deleteAllColumns();
await sleep(3000);

const promises = Array(clientCount)
.fill(1)
.map(() => paintAColumn(target, boardId));
return Promise.all([...fastEditorPromises]);
};

const setup = async ({
courseId,
configurations,
}: {
courseId: string;
configurations: Array<{ users: UserProfile[] }>;
}) => {
const urls = getUrlConfiguration();
const boardIds = await createBoards(urls.api, courseId, configurations.length);
const promises = configurations.flatMap((conf, index) => runBoardTest(boardIds[index], conf, urls));
await Promise.all(promises);
};

it('should run a basic load test', async () => {
// setup({ vars: { target: "https://main.dbc.dbildungscloud.dev", clients: 2 } });
await setup('http://localhost:4450', 30);
await setup({
courseId: '669a5a36c040408795c74581',
configurations: [viewersClass, viewersClass, collaborativeClass],
});
}, 600000);
});

// to run this test, you need to set the following environment variables:
// export token={ paste the token of a logged in user here }
// export target={ paste url of server }
// and you need to update the courseId above

// [x] refactor to typescript and move to server
// [x] add fetchCard
// [x] add pauses to scenarios
// [x] fix frontend richtext
// [x] fancy typing feature for richtext
// [x] define multi-board-scenarios
// [x] fetch cards of other users, too
// [x] implement different types of users
// [ ] failure handling
// [ ] fix problem with connection breakdown
// [ ] fetch cards with debounce
// [/] refactor clientFunctions to use request-parameters
// [ ] define multi-board-scenarios
// [ ] extract board generators
// [/] extract board generators
// [ ] evaluate using test factories
// [ ] fix moveCard
// [x] add delete methods
Expand Down
29 changes: 12 additions & 17 deletions apps/server/src/modules/board/loadtest/loadtestClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function createLoadtestClient(baseUrl: string, boardId: string, token: st

const waitForSuccess = async (eventName: string, options?: { checkIsOwnAction?: boolean; timeoutMs?: number }) =>
new Promise((resolve, reject) => {
const { checkIsOwnAction, timeoutMs } = { checkIsOwnAction: true, timeoutMs: 5000, ...options };
const { checkIsOwnAction, timeoutMs } = { checkIsOwnAction: true, timeoutMs: 500000, ...options };
const timeoutId = setTimeout(() => {
reject(new Error(`Timeout waiting for ${eventName}`));
}, timeoutMs);
Expand Down Expand Up @@ -88,6 +88,11 @@ export function createLoadtestClient(baseUrl: string, boardId: string, token: st
return result;
};

const fetchCard = async (payload: FetchCardsMessageParams) => {
const { newCard } = (await emitAndWait('fetch-card', payload)) as { newCard: CardResponse };
return newCard;
};

socket.on('connect', async () => {
console.log('connected');
await fetchBoard();
Expand All @@ -99,17 +104,12 @@ export function createLoadtestClient(baseUrl: string, boardId: string, token: st

socket.onAny(
(event: string, payload: { isOwnAction: boolean; newCard?: CardResponse; newColumn?: ColumnResponse }) => {
if (payload.isOwnAction === true) {
if (event === 'create-card-success' && payload.newCard) {
cards.push(payload.newCard);
}
if (event === 'create-column-success' && payload.newColumn) {
columns.push(payload.newColumn);
}
// console.log('socket.onAny', event);
// console.log(payload);
} else {
// console.log(event, payload);
if (event === 'create-card-success' && payload.newCard) {
cards.push(payload.newCard);
fetchCard({ cardIds: [payload.newCard.id] }).catch(console.error);
}
if (event === 'create-column-success' && payload.newColumn) {
columns.push(payload.newColumn);
}
}
);
Expand Down Expand Up @@ -150,11 +150,6 @@ export function createLoadtestClient(baseUrl: string, boardId: string, token: st
return newElement;
};

const fetchCard = async (payload: FetchCardsMessageParams) => {
const { newCard } = (await emitAndWait('fetch-card', payload)) as { newCard: CardResponse };
return newCard;
};

const moveCard = async (columnId: string, cardId: string, oldIndex: number, newIndex: number) => {
const result = await emitAndWait('move-card', {
cardId,
Expand Down

0 comments on commit 080d5bc

Please sign in to comment.