Skip to content

Commit

Permalink
feat(aws): improve compatibility with certain envs like AWS Lambda (#43)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Almouro authored Oct 26, 2022
1 parent 43e97e3 commit c8597be
Show file tree
Hide file tree
Showing 17 changed files with 507 additions and 398 deletions.
7 changes: 6 additions & 1 deletion packages/aws-device-farm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions packages/aws-device-farm/src/TMP_FOLDER.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TMP_FOLDER = "/tmp";
140 changes: 140 additions & 0 deletions packages/aws-device-farm/src/bin.ts
Original file line number Diff line number Diff line change
@@ -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 <apkPath>", "Path to the APK to be uploaded for testing")
.option(
"--testCommand <testCommand>",
"Test command that should be run (e.g.: `yarn jest appium`)"
)
.option(
"--testFolder <testFolder>",
"AWS requires us to upload the folder containing the tests including node_modules folder",
DEFAULT_RUN_TEST_OPTIONS.testFolder
)
.option(
"--testSpecsPath <testSpecsPath>",
"Path to yml config file driving the AWS Device Farm tests",
path.join(__dirname, "..", "flashlight.yml")
)
.option(
"--projectName <projectName>",
"AWS Device Farm project name",
DEFAULT_RUN_TEST_OPTIONS.projectName
)
.option(
"--testName <testName>",
"Test name to appear on AWS Device Farm",
DEFAULT_RUN_TEST_OPTIONS.testName
)
.option(
"--reportDestinationPath <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 <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 <apkUploadArn>",
"APK Upload ARN. Overrides apkPath option"
).env("APK_UPLOAD_ARN")
)
.option(
"--testFile <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 <testRunArn>", "Arn of the test run to check", ".")
.option(
"--reportDestinationPath <reportDestinationPath>",
"Folder where performance measures will be written",
"."
)
.action((options) => {
const { testRunArn, reportDestinationPath } = options;
checkResults({ testRunArn, reportDestinationPath });
});

program
.command("uploadApk")
.requiredOption(
"--apkPath <apkPath>",
"Path to the APK to be uploaded for testing"
)
.option(
"--projectName <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 <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();
35 changes: 35 additions & 0 deletions packages/aws-device-farm/src/commands/checkResults.ts
Original file line number Diff line number Diff line change
@@ -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`
);
};
Original file line number Diff line number Diff line change
@@ -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;
};
124 changes: 124 additions & 0 deletions packages/aws-device-farm/src/commands/runTest.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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;
};
Loading

0 comments on commit c8597be

Please sign in to comment.