@testlio/cli
Version:
Official Testlio platform command-line interface
727 lines (622 loc) • 25.4 kB
JavaScript
/* eslint-disable no-plusplus */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-param-reassign */
const fs = require('fs');
const axios = require('axios');
const path = require('path');
const Joi = require('joi');
const { sleep, allAllowedProviders } = require('./utils');
const FAILURE = 1;
const SUCCESS = 0;
const MAX_FAILED_POLLS = 3;
const POLLING_INTERVAL = 1000 * 30; // 30 seconds
const ONE_GB_IN_BYTES = 1024 ** 3;
const FILE_MAX_SIZE = 4 * ONE_GB_IN_BYTES;
const createRunSchema = Joi.object({
dryRun: Joi.boolean(),
pollResults: Joi.boolean(),
testArgs: Joi.string(),
externalResults: Joi.boolean().default(false),
automatedDeviceIds: Joi.array().items(Joi.string()),
automatedBrowserIds: Joi.array().items(Joi.string()),
resultProvider: Joi.string().when('externalResults', {
is: true,
then: Joi.required()
.valid(...allAllowedProviders)
.label(
`If externalResults flag is true then, please provide a valid result provider. Possible values are[${allAllowedProviders.join(
', '
)}]`
),
otherwise: Joi.optional()
}),
testConfig: Joi.string().default('test-config.json'),
projectConfig: Joi.string().default('project-config.json')
});
const executionConfigurationSchema = Joi.object({
videoCapture: Joi.boolean().default(true),
testArgs: Joi.string()
});
const configSchema = Joi.object({
deviceAppType: Joi.string().required(),
deviceTestType: Joi.string().required(),
type: Joi.string().required(),
executionConfiguration: executionConfigurationSchema
});
const configSchemaForExternalResults = Joi.object({
deviceTestType: Joi.string().optional()
});
const deviceSchema = Joi.object({
guids: Joi.array().items(Joi.string()),
oses: Joi.array().items(Joi.string()),
manufacturers: Joi.array().items(Joi.string()),
names: Joi.array().items(Joi.string()),
formFactors: Joi.array().items(Joi.string())
});
const browserSchema = Joi.object({
guids: Joi.array().items(Joi.string()),
browserNames: Joi.array().items(Joi.string()),
platformNames: Joi.array().items(Joi.string()),
versions: Joi.array().items(Joi.string())
});
const testConfigSchemaExternalResults = Joi.object({
automatedTestNamePrefix: Joi.string(),
config: configSchemaForExternalResults.optional()
});
const testConfigSchema = Joi.object({
automatedTestNamePrefix: Joi.string(),
config: configSchema.required(),
devices: deviceSchema,
browsers: browserSchema,
select: Joi.number().default(1),
testPackageURI: Joi.string().required(),
buildURI: Joi.string(),
deviceTimeoutInMinutes: Joi.number().positive().min(1).max(1400).default(120)
});
const projectConfigSchema = Joi.object({
baseURI: Joi.string().required(),
platformURI: Joi.string().required(),
projectId: Joi.number().required(),
automatedRunCollectionGuid: Joi.string().required(),
testRunCollectionGuid: Joi.string().required(),
workspaceName: Joi.string().required(),
resultCollectionGuid: Joi.string().required()
});
const createRun = async (testRunCollectionGuid, projectId, automatedTestNamePrefix) => {
const createAutomatedRunResponse = await axios.post(`/test-run/v1/collections/${testRunCollectionGuid}/runs`, {
startAt: new Date().toISOString(),
projectId,
type: 'Automated',
sourceType: 'testlio_cli'
});
console.log(createAutomatedRunResponse.status);
console.log('Step 1.1: Update automated run name');
const updateAutomatedTest = await axios.put(createAutomatedRunResponse?.data?.href, {
name: `${automatedTestNamePrefix} ${createAutomatedRunResponse.data.name}`
});
console.log(updateAutomatedTest.status);
return createAutomatedRunResponse;
};
const createAndUpdateAutomatedRunConfiguration = async (
config,
automatedRunCollectionGuid,
createAutomatedRunResponse,
testArgs,
provider,
deviceTimeoutInMinutes,
externalResults = false
) => {
console.log('Step 2.1: Search for automated run configuration');
const searchAutomatedRunConfigurationResponse = await axios.post(
`/automated-test-run/v1/collections/${automatedRunCollectionGuid}/search/run-configurations`,
{
runHrefs: [createAutomatedRunResponse.data.href]
}
);
const newAutomatedRunConfigurationHref = searchAutomatedRunConfigurationResponse?.data?.data[0].href;
console.log(newAutomatedRunConfigurationHref);
const configWithTestArgs = testArgs
? {
...config,
externalResults,
...(deviceTimeoutInMinutes ? { deviceTimeoutInMinutes } : {}),
...(provider ? { provider } : {}),
executionConfiguration: {
...config.executionConfiguration,
testArgs
}
}
: {
...config,
...(deviceTimeoutInMinutes ? { deviceTimeoutInMinutes } : {}),
externalResults,
provider
};
console.log('Step 2.2: Update automated runs configuration');
const updatedAutomatedRunConfig = await axios.put(newAutomatedRunConfigurationHref, configWithTestArgs);
console.log(updatedAutomatedRunConfig.status);
};
const createAutomatedRunPlan = async (createAutomatedRunResponse) => {
const automatedTestPlan = await axios.post(createAutomatedRunResponse.data.plans.href, {});
console.log(automatedTestPlan.data.href);
return automatedTestPlan.data.href;
};
const uploadBuild = async ({
buildFile,
buildFileName,
buildFileSize,
projectId,
automatedTestPlanHref,
isIosBuild = false
}) => {
console.log('Step 4.1: Get upload URL');
const uploadFileResponse = await axios.post(`/upload/v1/files`, { prefix: 'build-v3' });
console.log(uploadFileResponse.status);
console.log('Step 4.2: Upload build');
const response = await axios.put(uploadFileResponse.data.put.href, buildFile, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-length': buildFileSize,
'Content-Disposition': `attachment; filename=${encodeURIComponent(buildFileName)}`
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
transformRequest: (data, headers) => {
delete headers.common.Authorization;
return data;
}
});
console.log(response.status);
console.log('Step 4.3: Creating a new build');
console.log('Getting build collection href');
const projectCollectionResponse = await axios.get(`/project/v1/projects/${projectId}/collection`);
console.log(projectCollectionResponse.status);
console.log('Creating build');
let createBuildUri = `${projectCollectionResponse.data.buildCollectionHref}/builds/file`;
if (isIosBuild) {
createBuildUri += '?resign=true';
}
const createBuildResponse = await axios.post(createBuildUri, {
name: buildFileName,
url: uploadFileResponse.data.get.href,
version: '1.0.0'
});
console.log(createBuildResponse.status);
console.log('Step 4.4: Attach build to plan');
const attachBuild = await axios.post(`${automatedTestPlanHref}/builds`, {
buildHref: createBuildResponse.data.href,
useOriginal: true
});
console.log(attachBuild.status);
};
const uploadTestPackage = async ({
testPackageFile,
testPackageFileName,
testPackageFileSize,
automatedRunCollectionGuid,
automatedTestPlanHref
}) => {
console.log('Step 5.1: Get upload URL');
const uploadFileResponse = await axios.post(`/upload/v1/files`, { prefix: 'automated-run-attachment' });
console.log(uploadFileResponse.status);
console.log('Step 5.2: Upload test package');
console.log('Uploading test package to AWS');
const response = await axios.put(uploadFileResponse.data.put.href, testPackageFile, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-length': testPackageFileSize,
'Content-Disposition': `attachment; filename=${encodeURIComponent(testPackageFileName)}`
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
transformRequest: (data, headers) => {
delete headers.common.Authorization;
return data;
}
});
console.log(response.status);
console.log('Step 5.3: Create a new automated test package attachment');
const testPackage = await axios.post(
`/automated-test-run/v1/collections/${automatedRunCollectionGuid}/attachments`,
{
name: testPackageFileName || 'test-package-filename.zip',
fileType: 'application/zip',
attachmentType: 'TestPackage',
url: uploadFileResponse.data.get.href,
size: testPackageFileSize
}
);
console.log(testPackage.status);
console.log('Step 5.4: Attach test package attachment to plan');
const attachTestPackage = await axios.post(`${automatedTestPlanHref}/attachments`, {
attachmentHref: testPackage.data.href
});
console.log(attachTestPackage.status);
};
const searchBrowsers = async (payload, select) => {
const {
data: { data: foundBrowsers }
} = await axios.post('/automated-test-run/v1/browsers/search', payload);
const count = select || 1;
if (foundBrowsers.length < count) {
throw new Error('Cannot find enough browsers matching specified criteria');
}
const browsers = foundBrowsers.slice(0, count);
console.log('Selected browsers:');
browsers.forEach(({ browser, platform, version, id }, index) =>
console.log(`${index + 1}. ${browser} ${version}, ${platform} (guid: ${id})`)
);
return browsers;
};
const searchDevices = async (deviceAppType, payload, select) => {
const {
data: { data: foundDevices }
} = await axios.post('/automated-test-run/v1/devices/search', {
...payload,
platforms: [deviceAppType],
availability: ['HIGHLY_AVAILABLE', 'AVAILABLE']
});
const count = select || 1;
if (foundDevices.length < count) {
throw new Error('Cannot find enough available devices matching specified criteria');
}
const devices = foundDevices.slice(0, count);
console.log('Selected devices:');
devices.forEach(({ id, name, platform, os }, index) =>
console.log(`${index + 1}. ${name}, ${platform} ${os} (guid: ${id})`)
);
return devices;
};
const assignBrowsersToAutomatedRun = async (browsers, devicePoolFiltersHref) => {
await Promise.all(
browsers.map(async ({ id: automatedBrowserGuid }) => {
console.log(`Creating device pool filter for browser guid: ${automatedBrowserGuid}`);
const devicePoolFilter = await axios.post(devicePoolFiltersHref, {
automatedBrowsers: [{ automatedBrowserGuid }],
isGroup: false
});
console.log(devicePoolFilter.status);
})
);
};
const assignDevicesToAutomatedRun = async (devices, devicePoolFiltersHref) => {
const automatedDevices = devices.map(({ id: automatedDeviceGuid }) => ({ automatedDeviceGuid }));
const devicePoolFilter = await axios.post(devicePoolFiltersHref, {
automatedDevices,
isGroup: false
});
console.log(devicePoolFilter.status);
};
const attachDevicePoolFilters = async (
applicationType,
testRunCollectionGuid,
automatedTestPlanHref,
devices = null,
browsers = null
) => {
console.log('Step 6.1: Create device pool');
const devicePoolResponse = await axios.post(`/test-run/v1/collections/${testRunCollectionGuid}/device-pools`, {
description: null,
name: 'Automated device',
hidden: true,
withoutDeviceAssignment: true,
isGroup: false,
isMultipleDevices: false
});
console.log(devicePoolResponse.status);
console.log('Step 6.2: Create device pool filters');
const devicePoolFiltersHref = `${devicePoolResponse.data.href}/filters`;
if (applicationType === 'BROWSER' && browsers) {
await assignBrowsersToAutomatedRun(browsers, devicePoolFiltersHref);
} else if (applicationType === 'DEVICE' && devices) {
await assignDevicesToAutomatedRun(devices, devicePoolFiltersHref);
}
console.log('Step 6.3: Attaching device pool filter to automated run plan');
const attachPlanDevicePools = await axios.post(`${automatedTestPlanHref}/plan-device-pools`, {
jobsCount: 1,
devicePoolGuid: devicePoolResponse.data.guid
});
console.log(attachPlanDevicePools.status);
};
const scheduleRun = async (projectId, createAutomatedRunResponse) => {
return axios.put(createAutomatedRunResponse.data.href, {
status: 'inProgress',
projectId
});
};
const getRunResult = async (runHref, automatedRunCollectionGuid) => {
const response = await axios.get(
`/automated-test-run/v1/collections/${automatedRunCollectionGuid}/results-new?testRunHref=${runHref}`
);
console.log(response.status);
return response.data.results;
};
const validateDevice = async (deviceGuid) => {
try {
await axios.get(`/device/v1/automated-devices/${deviceGuid}`);
} catch (error) {
if (error.response?.status === 404) {
throw new Error(
`Invalid device selected: ${deviceGuid}. This device is not found in database, please check your parameters again or use (Step 1: Set up Automated Browser/Device)`
);
}
throw error;
}
};
const validateBrowser = async (browserGuid) => {
try {
await axios.get(`/browser/v1/automated-browsers/${browserGuid}`);
} catch (error) {
if (error.response?.status === 404) {
throw new Error(
`Invalid browser selected: ${browserGuid}. This browser is not found in database, please check your parameters again or use (Step 1: Set up Automated Browser/Device)`
);
}
throw error;
}
};
const pollRunResults = async (runHref, automatedRunCollectionGuid) => {
console.log(`Step 8: Starting to poll for results at an interval of ${POLLING_INTERVAL / 1000} seconds.`);
let runResult;
let failedRequests = 0;
while (!runResult) {
const response = await axios.get(
`/automated-test-run/v1/collections/${automatedRunCollectionGuid}/results-new?testRunHref=${runHref}`
);
console.log(response.status);
if (response.status !== 200) {
if (failedRequests >= MAX_FAILED_POLLS) throw new Error('Failed to poll results');
failedRequests++;
}
if (response.data.results.result) {
runResult = response.data.results.result;
break;
}
console.log('Polling - run still in progress');
await sleep(POLLING_INTERVAL);
}
return runResult;
};
const setAuthorizationToken = (token) => {
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
};
const setBaseUri = (baseURI) => {
axios.defaults.baseURL = baseURI;
};
const fileFromUri = (uri) => {
console.log('Reading file in form of stream from URI');
const file = fs.createReadStream(uri);
const fileName = path.basename(uri);
const { size: fileSize } = fs.statSync(uri);
if (fileSize > FILE_MAX_SIZE) {
throw new Error(`Uploading ${fileName} failed: max. file size ${FILE_MAX_SIZE / ONE_GB_IN_BYTES}GB`);
}
console.log(`File: ${fileName}`);
return { file, fileName, fileSize };
};
const printHelp = () => {
console.log(`Schedule a run
Usage:
testlio create-run
Options:
--projectConfig [path] path to project config (default: project-config.json)
--testConfig [path] path to test config (default: test-config.json)
--pollResults enables polling for run results
--externalResults enables external results
--automatedBrowserIds automated browser id(s)
--automatedDeviceIds automated device id(s)
--testArgs arguments to be passed to the test script execution command
--dryRun omit the actual run creation (used for testing)`);
};
module.exports = async (params) => {
const {
h,
help,
dryRun,
pollResults,
externalResults,
automatedDeviceIds,
automatedBrowserIds,
testArgs,
testConfig: testConfigFilePath,
projectConfig: projectConfigFilePath,
resultProvider
} = Joi.attempt(params, createRunSchema);
if (h || help) {
printHelp();
return;
}
if (!fs.existsSync(testConfigFilePath)) {
console.log(`File "${testConfigFilePath}" not found!`);
return FAILURE;
}
if (!fs.existsSync(projectConfigFilePath)) {
console.log(`File "${projectConfigFilePath}" not found!`);
return FAILURE;
}
try {
const testConfig = JSON.parse(fs.readFileSync(testConfigFilePath).toString());
const projectConfig = JSON.parse(fs.readFileSync(projectConfigFilePath).toString());
let testPackageFile;
let testPackageFileName;
let testPackageFileSize;
let type;
let deviceAppType;
const testConfigSchemaToUse = externalResults ? testConfigSchemaExternalResults : testConfigSchema;
let {
config = null,
automatedTestNamePrefix,
testPackageURI = null,
buildURI = null,
devices = null,
browsers = null,
select = null,
deviceTimeoutInMinutes = null
} = Joi.attempt(testConfig, testConfigSchemaToUse);
if (!externalResults) {
type = config.type;
deviceAppType = config.deviceAppType;
}
const { baseURI, testRunCollectionGuid, projectId, automatedRunCollectionGuid, platformURI, workspaceName } =
Joi.attempt(projectConfig, projectConfigSchema);
if (dryRun) {
console.log('Dry run mode. Run creation omitted.');
return SUCCESS;
}
if (!process.env.RUN_API_TOKEN) {
console.log('Please provide RUN API TOKEN');
return FAILURE;
}
if (!externalResults) {
const { file, fileName, fileSize } = fileFromUri(testPackageURI);
testPackageFile = file;
testPackageFileName = fileName;
testPackageFileSize = fileSize;
}
let buildFile;
let buildFileName;
let buildFileSize;
if (!externalResults && type === 'DEVICE' && deviceAppType !== 'WEB') {
const { file, fileName, fileSize } = fileFromUri(buildURI);
buildFile = file;
buildFileName = fileName;
buildFileSize = fileSize;
}
setBaseUri(baseURI);
setAuthorizationToken(process.env.RUN_API_TOKEN);
console.log('Starting process to create an automation run...');
let selectedDevices;
let selectedBrowsers;
if (externalResults === true) {
if (
(!Array.isArray(automatedDeviceIds) || !automatedDeviceIds?.length) &&
!Array.isArray(automatedBrowserIds || !automatedBrowserIds?.length)
) {
console.log(
'If external results are enabled, one or more automated device or browser ids must be provided i.e automatedDeviceIds or automatedBrowserIds'
);
return FAILURE;
}
selectedDevices = automatedDeviceIds?.map((id) => ({ id }));
selectedBrowsers = automatedBrowserIds?.map((id) => ({ id }));
if (selectedDevices?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const device of selectedDevices) {
console.log(`Validating device with id: ${device.id}`);
await validateDevice(device.id);
console.log(`Device with id: ${device.id} is valid`);
}
}
if (selectedBrowsers?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const browser of selectedBrowsers) {
console.log(`Validating browser with id: ${browser.id}`);
await validateBrowser(browser.id);
console.log(`Browser with id: ${browser.id} is valid`);
}
}
} else {
switch (type) {
case 'DEVICE':
console.log('Step 0: Search devices');
selectedDevices = await searchDevices(deviceAppType, devices, select);
break;
case 'BROWSER':
console.log('Step 0: Search browsers');
selectedBrowsers = await searchBrowsers(browsers, select);
break;
default:
console.log(`Unknown test type: ${type}`);
return FAILURE;
}
}
if (externalResults) {
config = {};
config.type = Array.isArray(automatedDeviceIds) || automatedDeviceIds?.length ? 'DEVICE' : 'BROWSER';
config.deviceAppType =
Array.isArray(automatedDeviceIds) || automatedDeviceIds?.length ? 'ANDROID' : 'OTHER';
type = config.type;
deviceAppType = config.deviceAppType;
}
console.log('Step 1: Create a new automated run');
const createAutomatedRunResponse = await createRun(testRunCollectionGuid, projectId, automatedTestNamePrefix);
console.log('Step 2: Create and Update automated run configuration');
await createAndUpdateAutomatedRunConfiguration(
config,
automatedRunCollectionGuid,
createAutomatedRunResponse,
externalResults ? testArgs : false,
resultProvider,
deviceTimeoutInMinutes,
externalResults
);
console.log('Step 3: Create automated run plan');
const automatedTestPlanHref = await createAutomatedRunPlan(createAutomatedRunResponse);
console.log('Step 4: Upload test build');
if (!externalResults && type === 'DEVICE' && deviceAppType !== 'WEB') {
await uploadBuild({
buildFile,
buildFileName,
buildFileSize,
projectId,
automatedTestPlanHref,
isIosBuild: deviceAppType === 'IOS'
});
} else {
console.log('Skipping the step as no build is required for Web testing');
}
if (!externalResults) {
console.log('Step 5: Upload test package');
await uploadTestPackage({
testPackageFile,
testPackageFileName,
testPackageFileSize,
automatedRunCollectionGuid,
automatedTestPlanHref
});
} else {
console.log('Skipping the step as no upload of the test package is required for external results');
}
console.log('Step 6: Devices');
await attachDevicePoolFilters(
type,
testRunCollectionGuid,
automatedTestPlanHref,
selectedDevices,
selectedBrowsers
);
console.log('Step 7. Schedule run! 🚀');
const scheduleRunResponse = await scheduleRun(projectId, createAutomatedRunResponse);
if (scheduleRunResponse.status === 200) {
console.log(
'Run is successfully scheduled, find automated run href: ',
`${platformURI}${workspaceName}/runs/${createAutomatedRunResponse.data.number}`
);
const runResult = await getRunResult(createAutomatedRunResponse.data.href, automatedRunCollectionGuid);
if (runResult && runResult.resultGuid) {
console.log(
`Writing result id ${runResult.resultGuid} to RESULT_ID environment variable in the .env file`
);
try {
// Set the terminal environment variable
fs.writeFileSync('.env', `RESULT_ID=${runResult.resultGuid}\n`);
console.log('RESULT_ID stored successfully');
} catch (error) {
console.error(`Error storing the RESULT_ID in .env file: ${error}`);
}
}
if (pollResults) {
const result = await pollRunResults(createAutomatedRunResponse.data.href, automatedRunCollectionGuid);
console.log('Polling - run finished with result: ', result);
return result === 'PASSED' ? SUCCESS : FAILURE;
}
return SUCCESS;
}
} catch (e) {
console.log(`Something went wrong while scheduling the automated run, with errorMessage: ${e.message}`);
console.error(e);
return FAILURE;
}
return FAILURE;
};