UNPKG

@testlio/cli

Version:

Official Testlio platform command-line interface

727 lines (622 loc) 25.4 kB
#! /usr/bin/env node /* 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; };