Skip to content

Commit

Permalink
feat(perftool): add beforeTest callback support, add request intercep…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
akhdrv committed Aug 1, 2023
1 parent bda0d5e commit d558dc8
Show file tree
Hide file tree
Showing 22 changed files with 334 additions and 210 deletions.
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@typescript-eslint/camelcase": "off",
"no-bitwise": "off",
"no-restricted-syntax": "off",
"no-underscore-dangle": "off",
"func-names": "off",
"no-return-assign": "off",
"generator-star-spacing": "off",
Expand All @@ -39,6 +40,7 @@
}
],
"padding-line-between-statements": "off",
"default-param-last": "off",
"implicit-arrow-linebreak": "off",
"no-plusplus": "off",
"max-classes-per-file": "off",
Expand All @@ -50,6 +52,7 @@
"function-paren-newline": "off",
"no-param-reassign": "off",
"no-shadow": "warn",
"no-eval": "warn",
"consistent-return": "off",
"prettier/prettier": "error",
"@typescript-eslint/explicit-function-return-type": "off",
Expand All @@ -66,6 +69,7 @@
"react/sort-comp": "off",
"react/no-array-index-key": "off",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "off",
"import/prefer-default-export": "off",
"import/order": [
"error",
Expand Down
3 changes: 3 additions & 0 deletions packages/perftool/lib/api/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { InterceptParams } from './intercept';

export const intercept = async (params: InterceptParams): Promise<void> => window._perftool_intercept?.(params);
130 changes: 130 additions & 0 deletions packages/perftool/lib/api/intercept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Page, HTTPRequest } from 'puppeteer';
import { minimatch } from 'minimatch';
import mime from 'mime';
import fsPromises from 'fs/promises';
import path from 'path';

import { JSONSerializable } from '../utils/types';
import BaseError from '../utils/baseError';
import { debug } from '../utils/logger';
import CWD from '../utils/cwd';

type Method = string;

export const enum FakeResponseType {
JSON = 'json',
File = 'file',
Abort = 'abort',
}

export type InterceptParams = {
method?: Method;
source: string;
} & (
| { responseType: FakeResponseType.Abort; response: undefined }
| { responseType: FakeResponseType.File; response: string }
| { responseType?: FakeResponseType.JSON; response: JSONSerializable }
);

type FakeResponse = { mimeType: string; data: string | Buffer };

class BadExtensionError extends BaseError {}
class BadPathError extends BaseError {}

const ANY_METHOD = 'ANY';

export async function useInterceptApi(page: Page): Promise<void> {
const requestReplacementByMethodMap: Map<Method, Map<string, FakeResponse | null>> = new Map();

function getRequestReplacement(url: string, method: Method): FakeResponse | null | void {
if (!requestReplacementByMethodMap.has(method)) {
return;
}

if (requestReplacementByMethodMap.get(method)!.has(url)) {
return requestReplacementByMethodMap.get(method)!.get(url);
}

for (const [source, response] of requestReplacementByMethodMap.get(method)!) {
if (minimatch(url, source)) {
return response;
}
}
}

async function setRequestReplacement({
method = ANY_METHOD,
response,
responseType = FakeResponseType.File,
source,
}: InterceptParams) {
if (!requestReplacementByMethodMap.has(method)) {
requestReplacementByMethodMap.set(method, new Map());
}

if (responseType === FakeResponseType.Abort) {
debug(`[Intercept] Set up ABORT ${method} ${source}`);
requestReplacementByMethodMap.get(method)!.set(source, null);
}

if (responseType === FakeResponseType.JSON) {
debug(`[Intercept] Set up JSON response ${method} ${source}`);
requestReplacementByMethodMap.get(method)!.set(source, {
mimeType: 'application/json',
data: JSON.stringify(response),
});
}

if (responseType === FakeResponseType.File) {
if (typeof response !== 'string') {
throw new BadPathError('Request replacement file path is not a string');
}

const mimeType = mime.getType(response);

if (!mimeType) {
throw new BadExtensionError('Could not get a mime type from file extension');
}

const data = await fsPromises.readFile(path.resolve(CWD, response));

debug(`[Intercept] Set up FILE response ${method} ${source} -> ${response}`);

requestReplacementByMethodMap.get(method)!.set(source, {
mimeType,
data,
});
}
}

async function handleInterceptedRequest(req: HTTPRequest) {
if (req.isInterceptResolutionHandled()) {
return;
}

const response = getRequestReplacement(req.url(), req.method()) || getRequestReplacement(req.url(), ANY_METHOD);

if (response == null) {
debug(`[Intercept] ${req.method()} ${req.url()} aborted`);
await req.abort();
return;
}

if (!response) {
await req.continue();
return;
}

await req.respond({
contentType: response.mimeType,
body: response.data,
});

debug(`[Intercept] ${req.method()} ${req.url()} intercepted, respond ${response.mimeType}`);
}

await page.exposeFunction('_perftool_intercept', setRequestReplacement);
await page.setRequestInterception(true);

page.on('request', handleInterceptedRequest);
}
8 changes: 4 additions & 4 deletions packages/perftool/lib/client/__spec__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ describe('client/createPerfToolClient', () => {
}));

jest.unstable_mockModule('../input', () => ({
resolveTests: (resolveTestsMock = jest.fn(() => [fakeTest])),
resolveTest: (resolveTestsMock = jest.fn(() => fakeTest)),
}));

window.finish = jest.fn() as typeof window.finish;
window._perftool_finish = jest.fn() as typeof window._perftool_finish;

const config = {} as Config;
const subjects = [{}] as Subject[];
Expand All @@ -48,7 +48,7 @@ describe('client/createPerfToolClient', () => {
state: fakeTest.state,
});

expect(window.finish).toHaveBeenCalledTimes(1);
expect(window.finish).toHaveBeenCalledWith([fakeResult]);
expect(window._perftool_finish).toHaveBeenCalledTimes(1);
expect(window._perftool_finish).toHaveBeenCalledWith(fakeResult);
});
});
90 changes: 42 additions & 48 deletions packages/perftool/lib/client/__spec__/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { jest } from '@jest/globals';
import type { Task } from '../measurement/types';
import type { Subject } from '../measurement/runner';

describe('client/input/getTests', () => {
describe('client/input/getTest', () => {
let assertMock = {} as jest.Mock;
beforeEach(() => {
jest.unstable_mockModule('../../utils/assert', () => ({
Expand All @@ -20,108 +20,102 @@ describe('client/input/getTests', () => {
jest.resetModules();
});

it('should transform raw tests with ids into tests with component and task', async () => {
it('should transform raw test with ids into test with component and task', async () => {
const fakeTasks = [{ id: 'fakeTaskId1' }, { id: 'fakeTaskId2' }, { id: 'fakeTaskId3' }] as Task<any, any>[];
const fakeSubjects = [
{ id: 'fakeSubjectId1' },
{ id: 'fakeSubjectId2' },
{ id: 'fakeSubjectId3' },
] as Subject[];
const state1 = { state1: '' };
const state2 = { state2: '' };
const fakeRawTests = [
{ subjectId: fakeSubjects[0].id, taskId: fakeTasks[0].id, state: state1 },
{ subjectId: fakeSubjects[1].id, taskId: fakeTasks[2].id, state: state2 },
];
const state = { state: '' };
const fakeRawTest = { subjectId: fakeSubjects[0].id, taskId: fakeTasks[0].id, state };

const { getTests } = await import('../input');
const { getTest } = await import('../input');

const result = getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects });
const result = getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects });

expect(result).toEqual([
{ subject: fakeSubjects[0], task: fakeTasks[0], state: state1 },
{ subject: fakeSubjects[1], task: fakeTasks[2], state: state2 },
]);

expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length);
expect(result).toEqual({ subject: fakeSubjects[0], task: fakeTasks[0], state });
expect(assertMock).toHaveBeenCalledTimes(1);
});

it('should throw if task not found', async () => {
const fakeTasks = [{ id: 'fakeTaskId1' }] as Task<any, any>[];
const fakeSubjects = [{ id: 'fakeSubjectId1' }] as Subject[];
const fakeRawTests = [{ subjectId: fakeSubjects[0].id, taskId: 'fakeTaskId2', state: {} }];
const fakeRawTest = { subjectId: fakeSubjects[0].id, taskId: 'fakeTaskId2', state: {} };

const { getTests } = await import('../input');
const { getTest } = await import('../input');

expect(() => {
getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects });
getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects });
}).toThrow();

expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length);
expect(assertMock).toHaveBeenCalledTimes(1);
});

it('should throw if subject not found', async () => {
const fakeTasks = [{ id: 'fakeTaskId1' }] as Task<any, any>[];
const fakeSubjects = [{ id: 'fakeSubjectId1' }] as Subject[];
const fakeRawTests = [{ subjectId: 'fakeSubjectId2', taskId: fakeTasks[0].id, state: {} }];
const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: fakeTasks[0].id, state: {} };

const { getTests } = await import('../input');
const { getTest } = await import('../input');

expect(() => {
getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects });
getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects });
}).toThrow();

expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length);
expect(assertMock).toHaveBeenCalledTimes(1);
});
});

describe('client/input/resolveTests', () => {
describe('client/input/resolveTest', () => {
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();

delete window.tests;
delete window._perftool_api_ready;
delete window._perftool_test;
});

it('should call getTests if window.tests is present', async () => {
const tasks = [] as Task<any, any>[];
const subjects = [] as Subject[];
const fakeRawTests = [] as any[];
const fakeResult = [] as any[];
it('should call getTests if window._perftool_test is present', async () => {
const tasks = [{ id: 'fakeTaskId' }] as any[];
const subjects = [{ id: 'fakeSubjectId2' }] as any[];
const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} };
const fakeResult = { subject: subjects[0], task: tasks[0], state: fakeRawTest.state };

window.tests = fakeRawTests;
window._perftool_test = fakeRawTest;

const { resolveTests } = await import('../input');
const result = await resolveTests({ tasks, subjects });
const { resolveTest } = await import('../input');
const result = await resolveTest({ tasks, subjects });

expect(result).toEqual(fakeResult);

expect(window.tests).toEqual(fakeRawTests);
expect(window._perftool_test).toEqual(fakeRawTest);
});

it("should imitate an array in window.tests if it's not present", async () => {
const tasks = [] as Task<any, any>[];
const subjects = [] as Subject[];
const fakeRawTests = [] as any[];
const fakeResult = [] as any[];
it("should create window._perftool_api_ready if test isn't present", async () => {
const tasks = [{ id: 'fakeTaskId' }] as any[];
const subjects = [{ id: 'fakeSubjectId2' }] as any[];
const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} };
const fakeResult = { subject: subjects[0], task: tasks[0], state: fakeRawTest.state };

const { resolveTests } = await import('../input');
const resultPromise = resolveTests({ tasks, subjects });
const { resolveTest } = await import('../input');
const resultPromise = resolveTest({ tasks, subjects });

window.tests?.push(...fakeRawTests);
window._perftool_test = fakeRawTest;
window._perftool_api_ready?.();

expect(resultPromise).resolves.toEqual(fakeResult);
});

it('should reject if error while transforming tests', async () => {
const tasks = [] as Task<any, any>[];
const subjects = [] as Subject[];
const fakeRawTests = [{ subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} }];
const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} };

const { resolveTests } = await import('../input');
const resultPromise = resolveTests({ tasks, subjects });
const { resolveTest } = await import('../input');
const resultPromise = resolveTest({ tasks, subjects });

window.tests?.push(...fakeRawTests);
window._perftool_test = fakeRawTest;
window._perftool_api_ready?.();

expect(resultPromise).rejects.toThrow();
});
Expand Down
19 changes: 5 additions & 14 deletions packages/perftool/lib/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { debug } from '../utils/logger';

import type { Task } from './measurement/types';
import { runTask, Subject } from './measurement/runner';
import { resolveTests } from './input';
import { resolveTest } from './input';

type CreatePerfToolClientParams<T extends Task<any, any>[]> = {
subjects: Subject[];
Expand All @@ -21,19 +21,10 @@ export async function createPerfToolClient<T extends Task<any, any, any>[]>({
debug('Available tasks: ', tasks);
debug('Config: ', config);

const tests = await resolveTests({ tasks, subjects });
const resultPromises = [];
const { task, subject, state } = await resolveTest({ tasks, subjects });

debug(`Running ${tests.length} tests`);
for (const { task, subject, state } of tests) {
const resultPromise = runTask<T[number]>({ task, subject, config, state });
debug(`Running...`);

resultPromises.push(resultPromise);
}

debug('Waiting for all tests to complete...');
const results = await Promise.all(resultPromises);

debug('All tests complete, calling window.finish');
await window.finish(results);
const result = await runTask<T[number]>({ task, subject, config, state });
await window._perftool_finish!(result);
}
Loading

0 comments on commit d558dc8

Please sign in to comment.