UNPKG

loadmill

Version:

A node.js module for running load tests and functional tests on loadmill.com

425 lines (371 loc) 13.9 kB
import './polyfills' import * as fs from 'fs'; import { HttpMethods, sendHttpRequest } from './http-request'; import { filterLabels, filterTags, TESTING_ORIGIN, toLoadmillParams, readRawParams, FLOW_STATUS, } from './utils'; import { junitReport as createJunitReport, mochawesomeReport as createMochawesomeReport } from './reporter'; const TEST_PLAN_POLL_INTERVAL_IN_MS = 10 * 1000 // 10 seconds export = Loadmill; function Loadmill(options: Loadmill.LoadmillOptions) { const { token, _testingServerHost } = options as any; const testingServer = _testingServerHost ? `https://${_testingServerHost}` : TESTING_ORIGIN; const testPlansAPI = `${testingServer}/api/test-plans`; async function _wait(testDefOrId: string | Loadmill.TestDef, callback?: Loadmill.Callback): Promise<Loadmill.TestResult> { let resolve, reject; const testDef = typeof testDefOrId === 'string' ? { id: testDefOrId, type: Loadmill.TYPES.LOAD, } : testDefOrId; const apiUrl = getTestAPIUrl(testDef, testingServer); const webUrl = getTestWebUrl(testDef, testingServer); let retries = 1; const intervalId = setInterval(async () => { try { let { body } = await sendHttpRequest({ url: apiUrl, token }); if (isTestInFinalState(body, testDef.type)) { clearInterval(intervalId); if (testDef.type === Loadmill.TYPES.TEST_PLAN) { const { body: bodyWithFlows } = await sendHttpRequest({ url: apiUrl, query: { fetchAllFlows: true, groupFlowAttempts: true, }, token, }); body = bodyWithFlows; } const testResult: Loadmill.TestResult = toTestResult(testDef, webUrl, body); redactData(testResult, body); if (callback) { callback(null, testResult); } else { resolve(testResult); } } } catch (err) { if (retries < 3) { retries++; return; } else { clearInterval(intervalId); } if (callback) { callback(err, null); } else { reject(err); } } }, TEST_PLAN_POLL_INTERVAL_IN_MS); return callback ? null! as Promise<any> : new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); } async function _runTestPlan( testPlan: Loadmill.TestPlanDef, params: Loadmill.Params, ) { const testPlanId = testPlan.id; const overrideParameters = toParams(params, testPlan.options?.parametersFile); const labels = testPlan.options && testPlan.options.labels && filterLabels(testPlan.options.labels); const labelsExpression = testPlan.options && testPlan.options.labelsExpression; const additionalDescription = testPlan.options && testPlan.options.additionalDescription; const pool = testPlan.options && testPlan.options.pool; const tags = testPlan.options && testPlan.options.tags && filterTags(testPlan.options.tags); const parallel = testPlan.options && testPlan.options.parallel; const branch = testPlan.options && testPlan.options.branch; const inlineParameterOverride = !!(testPlan.options && testPlan.options.inlineParameterOverride); const maxFlakyFlowRetries = testPlan.options && testPlan.options.maxFlakyFlowRetries; const apiCatalogService = testPlan.options && testPlan.options.apiCatalogService; const turboParallel = !!(testPlan.options && testPlan.options.turboParallel); const { body: { testPlanRunId, err } } = await sendHttpRequest({ method: HttpMethods.POST, url: `${testPlansAPI}/${testPlanId}/run`, body: { overrideParameters, additionalDescription, labels, pool, parallel, tags, branch, maxFlakyFlowRetries, labelsExpression, inlineParameterOverride, apiCatalogService, turboParallel, }, token, }); if (err || !testPlanRunId) { console.error(err ? JSON.stringify(err) : "The server encountered an error while handling the request"); return; } return { id: testPlanRunId, type: Loadmill.TYPES.TEST_PLAN }; } async function _junitReport(testResult: Loadmill.TestResult, path?: string) { return createJunitReport(testResult, token, path); } async function _mochawesomeReport(testResult: Loadmill.TestResult, path?: string) { return createMochawesomeReport(testResult, token, path); } return { run( config: Loadmill.Configuration, paramsOrCallback?: Loadmill.ParamsOrCallback, callback?: Loadmill.Callback): Promise<string> { return wrap( async () => { config = toConfig(config, paramsOrCallback); const { body: { testId } } = await sendHttpRequest({ method: HttpMethods.POST, url: testingServer + "/api/tests", body: config, token, }); await sendHttpRequest({ method: HttpMethods.PUT, url: `${testingServer}/api/tests/${testId}/load`, token, }); return testId; }, callback || paramsOrCallback ); }, wait(testDefOrId: string | Loadmill.TestDef, callback?: Loadmill.Callback): Promise<Loadmill.TestResult> { return _wait(testDefOrId, callback); }, async runTestPlan( testPlan: Loadmill.TestPlanDef, params: Loadmill.Params, ): Promise<Loadmill.TestDef | undefined> { return _runTestPlan(testPlan, params); }, async junitReport(testResult: Loadmill.TestResult, path?: string): Promise<void> { return _junitReport(testResult, path); }, async mochawesomeReport(testResult: Loadmill.TestResult, path?: string): Promise<void> { return _mochawesomeReport(testResult, path); }, }; } const isTestPassed = (body, type) => { switch (type) { case Loadmill.TYPES.SUITE: case Loadmill.TYPES.TEST_PLAN: return body.status === "PASSED"; default: //load return body.result === 'done'; } } function toTestResult(testDef: Loadmill.TestDef, webUrl: string, body: any): Loadmill.TestResult { return { ...testDef, url: webUrl, description: body && body.description, passed: isTestPassed(body, testDef.type), startTime: body.startTime, endTime: body.endTime, status: body.status, } } function redactData(testResult: Loadmill.TestResult, body: any) { testResult.testSuitesRuns = reductTestSuitesRuns(body.testSuitesRuns); } function isTestInFinalState(body, runType) { if (runType === Loadmill.TYPES.TEST_PLAN) { if (body.testSuitesRuns.some(suite => suite.status === "RUNNING")) { return false; } } const { trialResult, result, status } = body; return ( (result || trialResult === false) || // load tests (status && status !== "RUNNING") // test suites or test plan ); } function getTestAPIUrl({ id, type }: Loadmill.TestDef, server: string) { const prefix = `${server}/api`; switch (type) { case Loadmill.TYPES.SUITE: return `${prefix}/test-suites-runs/${id}` case Loadmill.TYPES.TEST_PLAN: return `${prefix}/test-plans-runs/${id}` default: //load return `${prefix}/tests/${id}`; } } function getTestWebUrl({ id, type }: Loadmill.TestDef, server: string) { const prefix = `${server}/app`; switch (type) { case Loadmill.TYPES.SUITE: return `${prefix}/api-tests/test-suite-runs/${id}` case Loadmill.TYPES.TEST_PLAN: return `${prefix}/api-tests/test-plan-runs/${id}` default: //load return `${prefix}/test/${id}` } } function reductTestSuitesRuns(suitesRuns) { if (suitesRuns) { return suitesRuns.map(s => { const suiteRun: Loadmill.TestResult = { id: s.id, type: Loadmill.TYPES.SUITE, description: s.description, status: s.status, url: s.url, passed: s.status === "PASSED", startTime: s.startTime, endTime: s.endTime, } s.error && (suiteRun.error = s.error.message); if (Array.isArray(s.testSuiteFlowRuns)) { suiteRun.flowRuns = s.testSuiteFlowRuns.map(fr => { const flowRun: Loadmill.FlowRun = { id: fr.id, status: fr.status, description: fr.description, flowStatus: fr.testSuiteFlowStatus, duration: (fr.endTime - fr.startTime) || 0, }; if (fr.runs?.length) { flowRun.retries = fr.runs.length - 1; flowRun.duration = fr.runs[fr.runs.length - 1].endTime - fr.runs[0].startTime; } return flowRun; }); } return suiteRun; }); } } function wrap(asyncFunction, paramsOrCallback?: Loadmill.ParamsOrCallback) { const promise = asyncFunction(); if (typeof paramsOrCallback === 'function') { promise.then(res => paramsOrCallback(null, res)) .catch(err => paramsOrCallback(err, null)); } else { return promise; } } function toConfig(config: any | string, paramsOrCallback?: Loadmill.ParamsOrCallback) { if (typeof config === 'string') { let text = fs.readFileSync(config).toString(); config = JSON.parse(text); } if (typeof paramsOrCallback === 'object' && paramsOrCallback != null) { let parameters = config.parameters; if (!parameters) { config.parameters = paramsOrCallback; } else if (typeof parameters.push === 'function') { parameters.push(paramsOrCallback); } else { config.parameters = [parameters, paramsOrCallback]; } } return config; } function toParams(params: Loadmill.Params = {}, filePath?: string) { if (filePath) { const fileParams = toLoadmillParams(readRawParams(filePath)); return { ...fileParams, ...params }; } return params; } namespace Loadmill { export interface LoadmillOptions { token: string; } export interface TestDef { id: string; type: TYPES; } export interface TestSuiteDef { id: string; description?: string; options?: TestSuiteOptions; } export interface TestPlanDef { id: string; description?: string; options?: TestPlanOptions; } export interface TestSuiteOptions { additionalDescription?: string; labels?: string[] | null; failGracefully?: boolean; pool?: string; parametersFile?: string; } export interface TestPlanOptions { additionalDescription?: string; labels?: string[] | null; labelsExpression?: string; fetchFlowRuns?: boolean; pool?: string; tags?: string[] | null; parallel?: number | string; branch?: string; maxFlakyFlowRetries?: number | string; parametersFile?: string; inlineParameterOverride?: boolean; apiCatalogService?: string; turboParallel?: boolean; } export interface TestResult extends TestDef { url: string; passed: boolean; description: string flowRuns?: Array<FlowRun>; testSuitesRuns?: Array<TestResult>; status?: string; startTime: string; endTime: string; error?: SuiteError; } export interface FlowRun { id: string; status: string; description: string; flowStatus: FLOW_STATUS; duration: number; retries?: number; } type SuiteError = { message: string }; export type Configuration = object | string | any; // todo: bad typescript export type Params = { [key: string]: string }; export type ParamsOrCallback = Params | Callback; export type Callback = { (err: Error | null, result: any): void } | undefined; export type Histogram = { [reason: string]: number }; export type TestFailures = { [reason: string]: { [histogram: string]: Histogram } }; export type Args = { verbose: boolean, colors?: boolean }; export enum TYPES { LOAD = 'load', SUITE = 'test-suite', TEST_PLAN = 'test-plan' }; }