From c8597be91ca19d51a47d2466aead8bb51fbdbc05 Mon Sep 17 00:00:00 2001 From: Alexandre Moureaux Date: Wed, 26 Oct 2022 09:25:06 +0200 Subject: [PATCH] feat(aws): improve compatibility with certain envs like AWS Lambda (#43) * fix(aws): fix for envs where unzip is unavailable Typically AWS Lambda * fix(aws): fix for envs where curl is unavailable Typically AWS Lambda * ref: move some files around * fix(aws): prefer writing to tmp folder Useful for AWS Lambda compatibility * ref(aws): extract repositories declarations * ref(aws): extract uploadApk * ref(aws): extract checkResults * ref(aws): extract createDefaultNodeTestPackage * ref(aws): make several params optional on runTest * ref(aws): make testCommand also optional * ref(aws): rename binary to bin.ts * feat(aws): add non binary entry point * ref(aws): make apkPath also optional Using TS would be a better option --- packages/aws-device-farm/package.json | 7 +- packages/aws-device-farm/src/TMP_FOLDER.ts | 1 + packages/aws-device-farm/src/bin.ts | 140 +++++++++++ .../src/commands/checkResults.ts | 35 +++ .../commands/createDefaultNodeTestPackage.ts | 58 +++++ .../aws-device-farm/src/commands/runTest.ts | 124 ++++++++++ .../aws-device-farm/src/commands/uploadApk.ts | 23 ++ .../aws-device-farm/src/createTestSpecFile.ts | 25 +- packages/aws-device-farm/src/downloadFile.ts | 13 - packages/aws-device-farm/src/index.ts | 145 +---------- .../aws-device-farm/src/repositories/index.ts | 28 +++ .../src/repositories/upload.ts | 2 +- packages/aws-device-farm/src/runTest.ts | 233 ------------------ .../aws-device-farm/src/utils/downloadFile.ts | 17 ++ packages/aws-device-farm/src/utils/unzip.ts | 17 ++ .../src/{ => utils}/uploadFile.ts | 0 yarn.lock | 37 ++- 17 files changed, 507 insertions(+), 398 deletions(-) create mode 100644 packages/aws-device-farm/src/TMP_FOLDER.ts create mode 100644 packages/aws-device-farm/src/bin.ts create mode 100644 packages/aws-device-farm/src/commands/checkResults.ts create mode 100644 packages/aws-device-farm/src/commands/createDefaultNodeTestPackage.ts create mode 100644 packages/aws-device-farm/src/commands/runTest.ts create mode 100644 packages/aws-device-farm/src/commands/uploadApk.ts delete mode 100644 packages/aws-device-farm/src/downloadFile.ts create mode 100644 packages/aws-device-farm/src/repositories/index.ts delete mode 100644 packages/aws-device-farm/src/runTest.ts create mode 100644 packages/aws-device-farm/src/utils/downloadFile.ts create mode 100644 packages/aws-device-farm/src/utils/unzip.ts rename packages/aws-device-farm/src/{ => utils}/uploadFile.ts (100%) diff --git a/packages/aws-device-farm/package.json b/packages/aws-device-farm/package.json index fda712bf..8aafda75 100644 --- a/packages/aws-device-farm/package.json +++ b/packages/aws-device-farm/package.json @@ -3,11 +3,16 @@ "version": "0.4.0", "main": "dist/index.js", "bin": { - "aws-device-farm-run-test": "dist/index.js" + "aws-device-farm-run-test": "dist/bin.js" }, "dependencies": { "@aws-sdk/client-device-farm": "^3.154.0", "@perf-profiler/logger": "^0.2.0", + "adm-zip": "^0.5.9", + "axios": "^1.1.3", "commander": "^9.4.0" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.0" } } diff --git a/packages/aws-device-farm/src/TMP_FOLDER.ts b/packages/aws-device-farm/src/TMP_FOLDER.ts new file mode 100644 index 00000000..11a8d3e5 --- /dev/null +++ b/packages/aws-device-farm/src/TMP_FOLDER.ts @@ -0,0 +1 @@ +export const TMP_FOLDER = "/tmp"; diff --git a/packages/aws-device-farm/src/bin.ts b/packages/aws-device-farm/src/bin.ts new file mode 100644 index 00000000..eaa8145f --- /dev/null +++ b/packages/aws-device-farm/src/bin.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +import path from "path"; +import { Option, program } from "commander"; +import { DEFAULT_RUN_TEST_OPTIONS, runTest } from "./commands/runTest"; +import { uploadApk } from "./commands/uploadApk"; +import { checkResults } from "./commands/checkResults"; +import { projectRepository } from "./repositories"; +import { createDefaultNodeTestPackage } from "./commands/createDefaultNodeTestPackage"; + +program + .command("runTest") + .option("--apkPath ", "Path to the APK to be uploaded for testing") + .option( + "--testCommand ", + "Test command that should be run (e.g.: `yarn jest appium`)" + ) + .option( + "--testFolder ", + "AWS requires us to upload the folder containing the tests including node_modules folder", + DEFAULT_RUN_TEST_OPTIONS.testFolder + ) + .option( + "--testSpecsPath ", + "Path to yml config file driving the AWS Device Farm tests", + path.join(__dirname, "..", "flashlight.yml") + ) + .option( + "--projectName ", + "AWS Device Farm project name", + DEFAULT_RUN_TEST_OPTIONS.projectName + ) + .option( + "--testName ", + "Test name to appear on AWS Device Farm", + DEFAULT_RUN_TEST_OPTIONS.testName + ) + .option( + "--reportDestinationPath ", + "Folder where performance measures will be written", + DEFAULT_RUN_TEST_OPTIONS.reportDestinationPath + ) + .option( + "--skipWaitingForResult", + "Skip waiting for test to be done after scheduling run.", + false + ) + .option( + "--deviceName ", + "Device on which to run tests. A device pool with devices containing this parameter in their model name will be created", + DEFAULT_RUN_TEST_OPTIONS.deviceName + ) + .addOption( + new Option( + "--apkUploadArn ", + "APK Upload ARN. Overrides apkPath option" + ).env("APK_UPLOAD_ARN") + ) + .option( + "--testFile ", + "Pass a test file instead. Overrides testCommand and testSpecsPath." + ) + .action(async (options) => { + // Just destructuring to have type checking on the parameters sent to runTest + const { + projectName, + testSpecsPath, + testFolder, + apkPath, + testName, + reportDestinationPath, + skipWaitingForResult, + testCommand, + deviceName, + apkUploadArn, + testFile, + } = options; + + const testRunArn = await runTest({ + apkPath, + testSpecsPath, + testFolder, + projectName, + testName, + testCommand, + deviceName, + apkUploadArn, + testFile, + }); + + if (!skipWaitingForResult) { + await checkResults({ testRunArn, reportDestinationPath }); + } + }); + +program + .command("checkResults") + .option("--testRunArn ", "Arn of the test run to check", ".") + .option( + "--reportDestinationPath ", + "Folder where performance measures will be written", + "." + ) + .action((options) => { + const { testRunArn, reportDestinationPath } = options; + checkResults({ testRunArn, reportDestinationPath }); + }); + +program + .command("uploadApk") + .requiredOption( + "--apkPath ", + "Path to the APK to be uploaded for testing" + ) + .option( + "--projectName ", + "AWS Device Farm project name", + DEFAULT_RUN_TEST_OPTIONS.projectName + ) + .action(async (options) => { + const { apkPath, projectName } = options; + uploadApk({ apkPath, projectName }); + }); + +program + .command("createDefaultNodeTestPackage") + .option( + "--projectName ", + "AWS Device Farm project name", + DEFAULT_RUN_TEST_OPTIONS.projectName + ) + .action(async (options) => { + const { projectName } = options; + const projectArn = await projectRepository.getOrCreate({ + name: projectName, + }); + await createDefaultNodeTestPackage({ projectArn }); + }); + +program.parse(); diff --git a/packages/aws-device-farm/src/commands/checkResults.ts b/packages/aws-device-farm/src/commands/checkResults.ts new file mode 100644 index 00000000..7a0697ee --- /dev/null +++ b/packages/aws-device-farm/src/commands/checkResults.ts @@ -0,0 +1,35 @@ +import fs from "fs"; +import { ArtifactType } from "@aws-sdk/client-device-farm"; +import { Logger } from "@perf-profiler/logger"; +import { execSync } from "child_process"; +import { testRepository } from "../repositories"; +import { TMP_FOLDER } from "../TMP_FOLDER"; +import { downloadFile } from "../utils/downloadFile"; +import { unzip } from "../utils/unzip"; + +export const checkResults = async ({ + testRunArn, + reportDestinationPath, +}: { + testRunArn: string; + reportDestinationPath: string; +}) => { + await testRepository.waitForCompletion({ arn: testRunArn }); + const url = await testRepository.getArtifactUrl({ + arn: testRunArn, + type: ArtifactType.CUSTOMER_ARTIFACT, + }); + const tmpFolder = `${TMP_FOLDER}/${new Date().getTime()}`; + fs.mkdirSync(tmpFolder); + + const LOGS_FILE_TMP_PATH = `${tmpFolder}/logs.zip`; + await downloadFile(url, LOGS_FILE_TMP_PATH); + + unzip(LOGS_FILE_TMP_PATH, tmpFolder); + execSync( + `rm ${LOGS_FILE_TMP_PATH} && mv ${tmpFolder}/*.json ${reportDestinationPath} && rm -rf ${tmpFolder}` + ); + Logger.success( + `Results available, run "npx @perf-profiler/web-reporter ${reportDestinationPath}" to see them` + ); +}; diff --git a/packages/aws-device-farm/src/commands/createDefaultNodeTestPackage.ts b/packages/aws-device-farm/src/commands/createDefaultNodeTestPackage.ts new file mode 100644 index 00000000..731fd28b --- /dev/null +++ b/packages/aws-device-farm/src/commands/createDefaultNodeTestPackage.ts @@ -0,0 +1,58 @@ +import { UploadType } from "@aws-sdk/client-device-farm"; +import fs from "fs"; +import { Logger } from "@perf-profiler/logger"; +import { execSync } from "child_process"; +import { zipTestFolder } from "../zipTestFolder"; +import { uploadRepository } from "../repositories"; + +export const DEFAULT_TEST_PACKAGE_NAME = + "__PERF_PROFILER_SINGLE_FILE__DEFAULT_TEST_FOLDER__"; + +export const createDefaultNodeTestPackage = async ({ + projectArn, +}: { + projectArn: string; +}) => { + const testFolder = `/tmp/${DEFAULT_TEST_PACKAGE_NAME}`; + fs.rmSync(testFolder, { force: true, recursive: true }); + fs.mkdirSync(testFolder); + + const SAMPLE_PACKAGE_JSON = `{ + "name": "test-folder", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \\"Error: no test specified\\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@bam.tech/appium-helper": "latest", + "@perf-profiler/appium-test-cases": "latest", + "@perf-profiler/e2e": "latest" + }, + "bundledDependencies": [ + "@bam.tech/appium-helper", + "@perf-profiler/appium-test-cases", + "@perf-profiler/e2e" + ] + }`; + + fs.writeFileSync(`${testFolder}/package.json`, SAMPLE_PACKAGE_JSON); + Logger.info("Installing profiler dependencies in test package..."); + execSync(`cd ${testFolder} && npm install`); + + const testFolderZipPath = zipTestFolder(testFolder); + + const arn = await uploadRepository.upload({ + projectArn, + name: DEFAULT_TEST_PACKAGE_NAME, + filePath: testFolderZipPath, + type: UploadType.APPIUM_NODE_TEST_PACKAGE, + }); + fs.rmSync(testFolder, { force: true, recursive: true }); + + return arn; +}; diff --git a/packages/aws-device-farm/src/commands/runTest.ts b/packages/aws-device-farm/src/commands/runTest.ts new file mode 100644 index 00000000..20c6a4ec --- /dev/null +++ b/packages/aws-device-farm/src/commands/runTest.ts @@ -0,0 +1,124 @@ +import { UploadType } from "@aws-sdk/client-device-farm"; +import path from "path"; +import fs from "fs"; +import { Logger } from "@perf-profiler/logger"; +import { createTestSpecFile } from "../createTestSpecFile"; +import { zipTestFolder } from "../zipTestFolder"; +import { + devicePoolRepository, + projectRepository, + testRepository, + uploadRepository, +} from "../repositories"; +import { + createDefaultNodeTestPackage, + DEFAULT_TEST_PACKAGE_NAME, +} from "./createDefaultNodeTestPackage"; + +export const DEFAULT_RUN_TEST_OPTIONS = { + testFolder: ".", + testSpecsPath: path.join(__dirname, "..", "flashlight.yml"), + projectName: "Flashlight", + testName: "Flashlight", + reportDestinationPath: ".", + deviceName: "A10s", +}; + +const getSingleFileTestFolderArn = async ({ + projectArn, +}: { + projectArn: string; +}) => { + const testPackageArn = ( + await uploadRepository.getByName({ + projectArn, + name: DEFAULT_TEST_PACKAGE_NAME, + type: UploadType.APPIUM_NODE_TEST_PACKAGE, + }) + )?.arn; + + if (testPackageArn) { + Logger.success("Found test folder with performance profiler upload"); + return testPackageArn; + } else { + return createDefaultNodeTestPackage({ projectArn }); + } +}; + +export const runTest = async ({ + projectName = DEFAULT_RUN_TEST_OPTIONS.projectName, + apkPath, + testSpecsPath = DEFAULT_RUN_TEST_OPTIONS.testSpecsPath, + testFolder = DEFAULT_RUN_TEST_OPTIONS.testFolder, + testName = DEFAULT_RUN_TEST_OPTIONS.testName, + testCommand, + deviceName = DEFAULT_RUN_TEST_OPTIONS.deviceName, + apkUploadArn: apkUploadArnGiven, + testFile, +}: { + projectName?: string; + apkPath?: string; + testSpecsPath?: string; + testFolder?: string; + testName?: string; + testCommand?: string; + deviceName?: string; + apkUploadArn?: string; + testFile?: string; +}): Promise => { + const projectArn = await projectRepository.getOrCreate({ name: projectName }); + const devicePoolArn = await devicePoolRepository.getOrCreate({ + projectArn, + deviceName, + }); + + let testPackageArn = null; + if (testFile) { + testPackageArn = await getSingleFileTestFolderArn({ projectArn }); + } else { + const testFolderZipPath = zipTestFolder(testFolder); + testPackageArn = await uploadRepository.upload({ + projectArn, + filePath: testFolderZipPath, + type: UploadType.APPIUM_NODE_TEST_PACKAGE, + }); + } + + let apkUploadArn; + + if (apkUploadArnGiven) { + apkUploadArn = apkUploadArnGiven; + } else if (apkPath) { + apkUploadArn = await uploadRepository.upload({ + projectArn, + filePath: apkPath, + type: UploadType.ANDROID_APP, + }); + } else { + throw new Error("Neither apkUploadArn nor apkPath was passed."); + } + + const newTestSpecPath = createTestSpecFile({ + testSpecsPath, + testCommand, + testFile, + }); + const testSpecArn = await uploadRepository.upload({ + projectArn, + filePath: newTestSpecPath, + type: UploadType.APPIUM_NODE_TEST_SPEC, + }); + fs.rmSync(newTestSpecPath); + + Logger.info("Starting test run..."); + const testRunArn = await testRepository.scheduleRun({ + projectArn, + apkUploadArn, + devicePoolArn, + testName, + testPackageArn, + testSpecArn, + }); + + return testRunArn; +}; diff --git a/packages/aws-device-farm/src/commands/uploadApk.ts b/packages/aws-device-farm/src/commands/uploadApk.ts new file mode 100644 index 00000000..ddf0bf35 --- /dev/null +++ b/packages/aws-device-farm/src/commands/uploadApk.ts @@ -0,0 +1,23 @@ +import { UploadType } from "@aws-sdk/client-device-farm"; +import { Logger } from "@perf-profiler/logger"; +import { projectRepository, uploadRepository } from "../repositories"; + +export const uploadApk = async ({ + apkPath, + projectName, +}: { + apkPath: string; + projectName: string; +}) => { + const projectArn = await projectRepository.getOrCreate({ + name: projectName, + }); + + const apkUploadArn = await uploadRepository.upload({ + projectArn, + filePath: apkPath, + type: UploadType.ANDROID_APP, + }); + + Logger.success(`APK uploaded: ${apkUploadArn}`); +}; diff --git a/packages/aws-device-farm/src/createTestSpecFile.ts b/packages/aws-device-farm/src/createTestSpecFile.ts index 462081f3..f357a552 100644 --- a/packages/aws-device-farm/src/createTestSpecFile.ts +++ b/packages/aws-device-farm/src/createTestSpecFile.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import { TMP_FOLDER } from "./TMP_FOLDER"; const getSingleTestFileYml = ({ testFile }: { testFile: string }) => { const testCode = fs.readFileSync(testFile); @@ -29,17 +30,23 @@ export const createTestSpecFile = ({ testFile, }: { testSpecsPath: string; - testCommand: string; + testCommand?: string; testFile?: string; }): string => { - const newContent = testFile - ? getSingleTestFileYml({ testFile }) - : getTestCommandYml({ - testSpecsPath, - testCommand, - }); - - const newSpecFilePath = `${ + let newContent; + + if (testFile) { + newContent = getSingleTestFileYml({ testFile }); + } else if (testCommand) { + newContent = getTestCommandYml({ + testSpecsPath, + testCommand, + }); + } else { + throw new Error("Neither testCommand nor testFile was passed."); + } + + const newSpecFilePath = `${TMP_FOLDER}/${ path.basename(testSpecsPath).split(".")[0] }_${new Date().getTime()}.yml`; diff --git a/packages/aws-device-farm/src/downloadFile.ts b/packages/aws-device-farm/src/downloadFile.ts deleted file mode 100644 index cacff1bb..00000000 --- a/packages/aws-device-farm/src/downloadFile.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Logger } from "@perf-profiler/logger"; -import { execSync } from "child_process"; - -export const downloadFile = async ( - url: string, - destinationFilePath: string -) => { - Logger.info(`Downloading ${destinationFilePath}...`); - - execSync(`curl "${url}" -o ${destinationFilePath}`); - - Logger.success(`Download of ${destinationFilePath} done`); -}; diff --git a/packages/aws-device-farm/src/index.ts b/packages/aws-device-farm/src/index.ts index 5ee9a563..09edae42 100644 --- a/packages/aws-device-farm/src/index.ts +++ b/packages/aws-device-farm/src/index.ts @@ -1,140 +1,5 @@ -#!/usr/bin/env node - -import path from "path"; -import { Option, program } from "commander"; -import { - checkResults, - createDefaultNodeTestPackage, - runTest, - uploadApk, -} from "./runTest"; - -const DEFAULT_PROJECT_NAME = "Flashlight"; - -program - .command("runTest") - .option("--apkPath ", "Path to the APK to be uploaded for testing") - .option( - "--testCommand ", - "Test command that should be run (e.g.: `yarn jest appium`)" - ) - .option( - "--testFolder ", - "AWS requires us to upload the folder containing the tests including node_modules folder", - "." - ) - .option( - "--testSpecsPath ", - "Path to yml config file driving the AWS Device Farm tests", - path.join(__dirname, "..", "flashlight.yml") - ) - .option( - "--projectName ", - "AWS Device Farm project name", - DEFAULT_PROJECT_NAME - ) - .option( - "--testName ", - "Test name to appear on AWS Device Farm", - "Flashlight" - ) - .option( - "--reportDestinationPath ", - "Folder where performance measures will be written", - "." - ) - .option( - "--skipWaitingForResult", - "Skip waiting for test to be done after scheduling run.", - false - ) - .option( - "--deviceName ", - "Device on which to run tests. A device pool with devices containing this parameter in their model name will be created", - "A10s" - ) - .addOption( - new Option( - "--apkUploadArn ", - "APK Upload ARN. Overrides apkPath option" - ).env("APK_UPLOAD_ARN") - ) - .option( - "--testFile ", - "Pass a test file instead. Overrides testCommand and testSpecsPath." - ) - .action(async (options) => { - // Just destructuring to have type checking on the parameters sent to runTest - const { - projectName, - testSpecsPath, - testFolder, - apkPath, - testName, - reportDestinationPath, - skipWaitingForResult, - testCommand, - deviceName, - apkUploadArn, - testFile, - } = options; - - const testRunArn = await runTest({ - apkPath, - testSpecsPath, - testFolder, - projectName, - testName, - testCommand, - deviceName, - apkUploadArn, - testFile, - }); - - if (!skipWaitingForResult) { - await checkResults({ testRunArn, reportDestinationPath }); - } - }); - -program - .command("checkResults") - .option("--testRunArn ", "Arn of the test run to check", ".") - .option( - "--reportDestinationPath ", - "Folder where performance measures will be written", - "." - ) - .action((options) => { - const { testRunArn, reportDestinationPath } = options; - checkResults({ testRunArn, reportDestinationPath }); - }); - -program - .command("uploadApk") - .requiredOption( - "--apkPath ", - "Path to the APK to be uploaded for testing" - ) - .option( - "--projectName ", - "AWS Device Farm project name", - DEFAULT_PROJECT_NAME - ) - .action(async (options) => { - const { apkPath, projectName } = options; - uploadApk({ apkPath, projectName }); - }); - -program - .command("createDefaultNodeTestPackage") - .option( - "--projectName ", - "AWS Device Farm project name", - DEFAULT_PROJECT_NAME - ) - .action(async (options) => { - const { projectName } = options; - await createDefaultNodeTestPackage({ projectName }); - }); - -program.parse(); +export * from "./commands/checkResults"; +export * from "./commands/createDefaultNodeTestPackage"; +export * from "./commands/runTest"; +export * from "./commands/uploadApk"; +export * from "./repositories"; diff --git a/packages/aws-device-farm/src/repositories/index.ts b/packages/aws-device-farm/src/repositories/index.ts new file mode 100644 index 00000000..206637d6 --- /dev/null +++ b/packages/aws-device-farm/src/repositories/index.ts @@ -0,0 +1,28 @@ +import { DeviceFarmClient } from "@aws-sdk/client-device-farm"; +import { DevicePoolRepository } from "./devicePool"; +import { ProjectRepository } from "./project"; +import { TestRepository } from "./test"; +import { UploadRepository } from "./upload"; + +const DEFAULT_REGION = "us-west-2"; + +const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = process.env; + +if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { + throw new Error( + "Please provide AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables" + ); +} + +const client = new DeviceFarmClient({ + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }, + region: DEFAULT_REGION, +}); + +export const projectRepository = new ProjectRepository(client); +export const devicePoolRepository = new DevicePoolRepository(client); +export const uploadRepository = new UploadRepository(client); +export const testRepository = new TestRepository(client); diff --git a/packages/aws-device-farm/src/repositories/upload.ts b/packages/aws-device-farm/src/repositories/upload.ts index 5016f337..175714b4 100644 --- a/packages/aws-device-farm/src/repositories/upload.ts +++ b/packages/aws-device-farm/src/repositories/upload.ts @@ -8,7 +8,7 @@ import { import path from "path"; import { Logger } from "@perf-profiler/logger"; import { BaseRepository } from "./BaseRepository"; -import { uploadFile } from "../uploadFile"; +import { uploadFile } from "../utils/uploadFile"; export class UploadRepository extends BaseRepository { async isUploadSucceeded({ arn }: { arn: string }) { diff --git a/packages/aws-device-farm/src/runTest.ts b/packages/aws-device-farm/src/runTest.ts deleted file mode 100644 index 3efb2653..00000000 --- a/packages/aws-device-farm/src/runTest.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { - ArtifactType, - DeviceFarmClient, - UploadType, -} from "@aws-sdk/client-device-farm"; -import fs from "fs"; -import { Logger } from "@perf-profiler/logger"; -import { execSync } from "child_process"; -import { createTestSpecFile } from "./createTestSpecFile"; -import { downloadFile } from "./downloadFile"; -import { DevicePoolRepository } from "./repositories/devicePool"; -import { ProjectRepository } from "./repositories/project"; -import { TestRepository } from "./repositories/test"; -import { UploadRepository } from "./repositories/upload"; -import { zipTestFolder } from "./zipTestFolder"; - -const DEFAULT_REGION = "us-west-2"; - -const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = process.env; - -if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { - throw new Error( - "Please provide AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables" - ); -} - -const client = new DeviceFarmClient({ - credentials: { - accessKeyId: AWS_ACCESS_KEY_ID, - secretAccessKey: AWS_SECRET_ACCESS_KEY, - }, - region: DEFAULT_REGION, -}); - -const projectRepository = new ProjectRepository(client); -const devicePoolRepository = new DevicePoolRepository(client); -const uploadRepository = new UploadRepository(client); -const testRepository = new TestRepository(client); - -const NAME = "__PERF_PROFILER_SINGLE_FILE__DEFAULT_TEST_FOLDER__"; - -const _createDefaultNodeTestPackage = async ({ - projectArn, -}: { - projectArn: string; -}) => { - const testFolder = `/tmp/${NAME}`; - fs.rmSync(testFolder, { force: true, recursive: true }); - fs.mkdirSync(testFolder); - - const SAMPLE_PACKAGE_JSON = `{ - "name": "test-folder", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \\"Error: no test specified\\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@bam.tech/appium-helper": "latest", - "@perf-profiler/appium-test-cases": "latest", - "@perf-profiler/e2e": "latest" - }, - "bundledDependencies": [ - "@bam.tech/appium-helper", - "@perf-profiler/appium-test-cases", - "@perf-profiler/e2e" - ] - }`; - - fs.writeFileSync(`${testFolder}/package.json`, SAMPLE_PACKAGE_JSON); - Logger.info("Installing profiler dependencies in test package..."); - execSync(`cd ${testFolder} && npm install`); - - const testFolderZipPath = zipTestFolder(testFolder); - - const arn = await uploadRepository.upload({ - projectArn, - name: NAME, - filePath: testFolderZipPath, - type: UploadType.APPIUM_NODE_TEST_PACKAGE, - }); - fs.rmSync(testFolder, { force: true, recursive: true }); - - return arn; -}; - -export const createDefaultNodeTestPackage = async ({ - projectName, -}: { - projectName: string; -}) => { - const projectArn = await projectRepository.getOrCreate({ name: projectName }); - return _createDefaultNodeTestPackage({ projectArn }); -}; - -const getSingleFileTestFolderArn = async ({ - projectArn, -}: { - projectArn: string; -}) => { - const testPackageArn = ( - await uploadRepository.getByName({ - projectArn, - name: NAME, - type: UploadType.APPIUM_NODE_TEST_PACKAGE, - }) - )?.arn; - - if (testPackageArn) { - Logger.success("Found test folder with performance profiler upload"); - return testPackageArn; - } else { - return _createDefaultNodeTestPackage({ projectArn }); - } -}; - -export const runTest = async ({ - projectName, - apkPath, - testSpecsPath, - testFolder, - testName, - testCommand, - deviceName, - apkUploadArn: apkUploadArnGiven, - testFile, -}: { - projectName: string; - apkPath: string; - testSpecsPath: string; - testFolder: string; - testName: string; - testCommand: string; - deviceName: string; - apkUploadArn?: string; - testFile?: string; -}): Promise => { - const projectArn = await projectRepository.getOrCreate({ name: projectName }); - const devicePoolArn = await devicePoolRepository.getOrCreate({ - projectArn, - deviceName, - }); - - let testPackageArn = null; - if (testFile) { - testPackageArn = await getSingleFileTestFolderArn({ projectArn }); - } else { - const testFolderZipPath = zipTestFolder(testFolder); - testPackageArn = await uploadRepository.upload({ - projectArn, - filePath: testFolderZipPath, - type: UploadType.APPIUM_NODE_TEST_PACKAGE, - }); - } - - const apkUploadArn = - apkUploadArnGiven || - (await uploadRepository.upload({ - projectArn, - filePath: apkPath, - type: UploadType.ANDROID_APP, - })); - - const newTestSpecPath = createTestSpecFile({ - testSpecsPath, - testCommand, - testFile, - }); - const testSpecArn = await uploadRepository.upload({ - projectArn, - filePath: newTestSpecPath, - type: UploadType.APPIUM_NODE_TEST_SPEC, - }); - fs.rmSync(newTestSpecPath); - - Logger.info("Starting test run..."); - const testRunArn = await testRepository.scheduleRun({ - projectArn, - apkUploadArn, - devicePoolArn, - testName, - testPackageArn, - testSpecArn, - }); - - return testRunArn; -}; - -export const checkResults = async ({ - testRunArn, - reportDestinationPath, -}: { - testRunArn: string; - reportDestinationPath: string; -}) => { - await testRepository.waitForCompletion({ arn: testRunArn }); - const url = await testRepository.getArtifactUrl({ - arn: testRunArn, - type: ArtifactType.CUSTOMER_ARTIFACT, - }); - const LOGS_FILE_TMP_PATH = "logs.zip"; - downloadFile(url, LOGS_FILE_TMP_PATH); - execSync( - `rm -rf Host_Machine_Files && unzip ${LOGS_FILE_TMP_PATH} && rm ${LOGS_FILE_TMP_PATH} && mv Host_Machine_Files/\\$DEVICEFARM_LOG_DIR/*.json ${reportDestinationPath} && rm -rf Host_Machine_Files` - ); - Logger.success( - `Results available, run "npx @perf-profiler/web-reporter ${reportDestinationPath}" to see them` - ); -}; - -export const uploadApk = async ({ - apkPath, - projectName, -}: { - apkPath: string; - projectName: string; -}) => { - const projectArn = await projectRepository.getOrCreate({ - name: projectName, - }); - - const apkUploadArn = await uploadRepository.upload({ - projectArn, - filePath: apkPath, - type: UploadType.ANDROID_APP, - }); - - Logger.success(`APK uploaded: ${apkUploadArn}`); -}; diff --git a/packages/aws-device-farm/src/utils/downloadFile.ts b/packages/aws-device-farm/src/utils/downloadFile.ts new file mode 100644 index 00000000..ad1c5f4a --- /dev/null +++ b/packages/aws-device-farm/src/utils/downloadFile.ts @@ -0,0 +1,17 @@ +import axios from "axios"; +import fs from "fs"; + +// AWS Lambda don't have curl +export const downloadFile = async ( + url: string, + destinationFilePath: string +) => { + const { data } = await axios.get(url, { + responseType: "arraybuffer", // Important + headers: { + "Content-Type": "application/gzip", + }, + }); + + fs.writeFileSync(destinationFilePath, data); +}; diff --git a/packages/aws-device-farm/src/utils/unzip.ts b/packages/aws-device-farm/src/utils/unzip.ts new file mode 100644 index 00000000..a5322825 --- /dev/null +++ b/packages/aws-device-farm/src/utils/unzip.ts @@ -0,0 +1,17 @@ +// AWS Lamda doesn't have unzip +import AdmZip from "adm-zip"; +import fs from "fs"; + +export const unzip = (path: string, destinationFolder: string) => { + const zip = new AdmZip(path); + const zipEntries = zip.getEntries(); + + for (const zipEntry of zipEntries) { + const parts = zipEntry.entryName.split("/"); + + fs.writeFileSync( + `${destinationFolder}/${parts[parts.length - 1]}`, + zipEntry.getData().toString("utf8") + ); + } +}; diff --git a/packages/aws-device-farm/src/uploadFile.ts b/packages/aws-device-farm/src/utils/uploadFile.ts similarity index 100% rename from packages/aws-device-farm/src/uploadFile.ts rename to packages/aws-device-farm/src/utils/uploadFile.ts diff --git a/yarn.lock b/yarn.lock index ba17bf67..dea34244 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4144,6 +4144,13 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/adm-zip@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.0.tgz#94c90a837ce02e256c7c665a6a1eb295906333c1" + integrity sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw== + dependencies: + "@types/node" "*" + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" @@ -4677,6 +4684,11 @@ add-stream@^1.0.0: resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= +adm-zip@^0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" + integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== + agent-base@5: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" @@ -5085,6 +5097,15 @@ axios@^0.21.1: dependencies: follow-redirects "^1.14.0" +axios@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" + integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -7308,6 +7329,11 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -7322,6 +7348,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -10947,7 +10982,7 @@ protocols@^1.1.0, protocols@^1.4.0: resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== -proxy-from-env@^1.0.0: +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==