Skip to content

Commit

Permalink
Performance Test service and config refactor (#128258)
Browse files Browse the repository at this point in the history
* add build id, job id and execution id to playwright config

* change apm configs (serverUrl, secretToken)

* refactor single user journeys to break "test" concept around them

* Authentication service and Login Journey

* move test steps to runUserJourney as argument

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
suchcodemuchwow and kibanamachine authored Apr 12, 2022
1 parent 67ef4f7 commit cbf0cea
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 293 deletions.
3 changes: 0 additions & 3 deletions .buildkite/scripts/steps/functional/performance_playwright.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ sleep 120

cd "$XPACK_DIR"

jobId=$(npx uuid)
export TEST_JOB_ID="$jobId"

journeys=("ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard")

for i in "${journeys[@]}"; do
Expand Down
7 changes: 5 additions & 2 deletions x-pack/test/performance/config.playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext

const testFiles = [require.resolve('./tests/playwright')];

const testJobId = process.env.TEST_JOB_ID ?? uuid();
log.info(`👷 JOB ID ${testJobId}👷`);
const testBuildId = process.env.BUILDKITE_BUILD_ID ?? `local-${uuid()}`;
const testJobId = process.env.BUILDKITE_JOB_ID ?? `local-${uuid()}`;
const executionId = uuid();

log.info(` 👷‍♀️ BUILD ID ${testBuildId}\n 👷 JOB ID ${testJobId}\n 👷‍♂️ EXECUTION ID:${executionId}`);

return {
testFiles,
Expand Down
62 changes: 62 additions & 0 deletions x-pack/test/performance/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import axios, { AxiosResponse } from 'axios';
import Url from 'url';
import { FtrService, FtrProviderContext } from '../ftr_provider_context';

export interface Credentials {
username: string;
password: string;
}

function extractCookieValue(authResponse: AxiosResponse) {
return authResponse.headers['set-cookie'][0].toString().split(';')[0].split('sid=')[1] as string;
}
export class AuthService extends FtrService {
private readonly kibanaServer = this.ctx.getService('kibanaServer');
private readonly config = this.ctx.getService('config');

constructor(ctx: FtrProviderContext) {
super(ctx);
}

public async login({ username, password }: Credentials) {
const headers = {
'content-type': 'application/json',
'kbn-version': await this.kibanaServer.version.get(),
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
};

const baseUrl = Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});

const loginUrl = baseUrl + '/internal/security/login';
const provider = baseUrl.includes('localhost') ? 'basic' : 'cloud-basic';

const authBody = {
providerType: 'basic',
providerName: provider,
currentURL: `${baseUrl}/login?next=%2F`,
params: { username, password },
};

const authResponse = await axios.post(loginUrl, authBody, { headers });

return {
name: 'sid',
value: extractCookieValue(authResponse),
url: baseUrl,
};
}
}

export const AuthProvider = (ctx: FtrProviderContext) => new AuthService(ctx);
2 changes: 2 additions & 0 deletions x-pack/test/performance/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { services as functionalServices } from '../../functional/services';
import { PerformanceTestingService } from './performance';
import { InputDelaysProvider } from './input_delays';
import { AuthProvider } from './auth';

export const services = {
es: functionalServices.es,
Expand All @@ -16,4 +17,5 @@ export const services = {
retry: functionalServices.retry,
performance: PerformanceTestingService,
inputDelays: InputDelaysProvider,
auth: AuthProvider,
};
193 changes: 80 additions & 113 deletions x-pack/test/performance/services/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,46 @@
import Url from 'url';
import { inspect } from 'util';
import apm, { Span, Transaction } from 'elastic-apm-node';
import { setTimeout } from 'timers/promises';
import playwright, { ChromiumBrowser, Page, BrowserContext } from 'playwright';
import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession } from 'playwright';
import { FtrService, FtrProviderContext } from '../ftr_provider_context';

type StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;

apm.start({
secretToken: 'Q5q5rWQEw6tKeirBpw',
serverUrl: 'https://2fad4006bf784bb8a54e52f4a5862609.apm.us-west1.gcp.cloud.es.io:443',
serviceName: 'functional test runner',
serverUrl: 'https://kibana-ops-e2e-perf.apm.us-central1.gcp.cloud.es.io:443',
secretToken: 'CTs9y3cvcfq13bQqsB',
});

interface StepCtx {
export interface StepCtx {
page: Page;
kibanaUrl: string;
}

type StepFn = (ctx: StepCtx) => Promise<void>;
type Steps = Array<{ name: string; fn: StepFn }>;
export type Steps = Array<{ name: string; handler: StepFn }>;

export class PerformanceTestingService extends FtrService {
private readonly auth = this.ctx.getService('auth');
private readonly config = this.ctx.getService('config');
private readonly lifecycle = this.ctx.getService('lifecycle');
private readonly inputDelays = this.ctx.getService('inputDelays');

private browser: ChromiumBrowser | undefined;
private storageState: StorageState | undefined;
private currentSpanStack: Array<Span | null> = [];
private currentTransaction: Transaction | undefined | null;
private currentTransaction: Transaction | undefined | null = undefined;

constructor(ctx: FtrProviderContext) {
super(ctx);
}

this.lifecycle.beforeTests.add(async () => {
await this.withTransaction('Journey setup', async () => {
await this.getStorageState();
});
});

this.lifecycle.cleanup.add(async () => {
apm.flush();
await setTimeout(5000);
await this.browser?.close();
private getKibanaUrl() {
return Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});
}

private async withTransaction<T>(name: string, block: () => Promise<T>) {
try {
if (this.currentTransaction !== undefined) {
if (this.currentTransaction) {
throw new Error(
`Transaction already started, make sure you end transaction ${this.currentTransaction?.name}`
);
Expand Down Expand Up @@ -105,42 +99,6 @@ export class PerformanceTestingService extends FtrService {
?.traceparent;
}

private async getStorageState() {
if (this.storageState) {
return this.storageState;
}

await this.withSpan('initial login', undefined, async () => {
const kibanaUrl = Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});

const browser = await this.getBrowserInstance();
const context = await browser.newContext();
const page = await context.newPage();
await this.interceptBrowserRequests(page);
await page.goto(`${kibanaUrl}`);

const usernameLocator = page.locator('[data-test-subj=loginUsername]');
const passwordLocator = page.locator('[data-test-subj=loginPassword]');
const submitButtonLocator = page.locator('[data-test-subj=loginSubmit]');

await usernameLocator?.type('elastic', { delay: this.inputDelays.TYPING });
await passwordLocator?.type('changeme', { delay: this.inputDelays.TYPING });
await submitButtonLocator?.click({ delay: this.inputDelays.MOUSE_CLICK });

await page.waitForSelector('#headerUserMenu');

this.storageState = await page.context().storageState();
await page.close();
await context.close();
});

return this.storageState;
}

private async getBrowserInstance() {
if (this.browser) {
return this.browser;
Expand Down Expand Up @@ -179,70 +137,79 @@ export class PerformanceTestingService extends FtrService {
});
}

public makePage(journeyName: string) {
const steps: Steps = [];

it(journeyName, async () => {
await this.withTransaction(`Journey ${journeyName}`, async () => {
const browser = await this.getBrowserInstance();
const context = await browser.newContext({
viewport: { width: 1600, height: 1200 },
storageState: await this.getStorageState(),
});

const page = await context.newPage();
page.on('console', (message) => {
(async () => {
try {
const args = await Promise.all(
message.args().map(async (handle) => handle.jsonValue())
);
public runUserJourney(
journeyName: string,
steps: Steps,
{ requireAuth }: { requireAuth: boolean }
) {
return this.withTransaction(`Journey ${journeyName}`, async () => {
const browser = await this.getBrowserInstance();
const viewport = { width: 1600, height: 1200 };
const context = await browser.newContext({ viewport });

const { url, lineNumber, columnNumber } = message.location();
if (!requireAuth) {
const cookie = await this.auth.login({ username: 'elastic', password: 'changeme' });
await context.addCookies([cookie]);
}

const location = `${url},${lineNumber},${columnNumber}`;
const page = await context.newPage();
if (!process.env.NO_BROWSER_LOG) {
page.on('console', this.onConsoleEvent());
}
const client = await this.sendCDPCommands(context, page);

const text = args.length
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' ')
: message.text();
await this.interceptBrowserRequests(page);
await this.handleSteps(steps, page);
await this.tearDown(page, client, context);
});
}

console.log(`[console.${message.type()}]`, text);
console.log(' ', location);
} catch (e) {
console.error('Failed to evaluate console.log line', e);
}
})();
});
const client = await this.sendCDPCommands(context, page);
private async tearDown(page: Page, client: CDPSession, context: BrowserContext) {
if (page) {
apm.flush();
await client.detach();
await page.close();
await context.close();
}
}

await this.interceptBrowserRequests(page);
public async shutdownBrowser() {
if (this.browser) {
await (await this.getBrowserInstance()).close();
}
}

private async handleSteps(steps: Array<{ name: string; handler: StepFn }>, page: Page) {
for (const step of steps) {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
try {
for (const step of steps) {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
try {
await step.fn({ page });
} catch (e) {
const error = new Error(`Step [${step.name}] failed: ${e.message}`);
error.stack = e.stack;
throw error;
}
});
}
} finally {
if (page) {
await client.detach();
await page.close();
await context.close();
}
await step.handler({ page, kibanaUrl: this.getKibanaUrl() });
} catch (e) {
const error = new Error(`Step [${step.name}] failed: ${e.message}`);
error.stack = e.stack;
}
});
});
}
}

private onConsoleEvent() {
return async (message: playwright.ConsoleMessage) => {
try {
const args = await Promise.all(message.args().map(async (handle) => handle.jsonValue()));

const { url, lineNumber, columnNumber } = message.location();

return {
step: (name: string, fn: StepFn) => {
steps.push({ name, fn });
},
const location = `${url},${lineNumber},${columnNumber}`;

const text = args.length
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg, false, null))).join(' ')
: message.text();

console.log(`[console.${message.type()}]`, text);
console.log(' ', location);
} catch (e) {
console.error('Failed to evaluate console.log line', e);
}
};
}
}
Loading

0 comments on commit cbf0cea

Please sign in to comment.