-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
17 changed files
with
507 additions
and
398 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const TMP_FOLDER = "/tmp"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
); | ||
}; |
58 changes: 58 additions & 0 deletions
58
packages/aws-device-farm/src/commands/createDefaultNodeTestPackage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
Oops, something went wrong.