Skip to content

Commit

Permalink
feat: Add LambdaTest support for E2E automation
Browse files Browse the repository at this point in the history
Signed-off-by: Filipe-Santos <[email protected]>
  • Loading branch information
fc-santos committed May 12, 2023
1 parent 2449405 commit 4734397
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 131 deletions.
143 changes: 49 additions & 94 deletions README.md

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions aries-mobile-tests/device_service_handler/lambda_test_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Class for actual LambdaTest Device Service Handling
"""

from device_service_handler.device_service_handler_interface import DeviceServiceHandlerInterface
import json
from decouple import config
from appium import webdriver
import requests
import aiohttp
import asyncio
import os



class LambdaTestHandler(DeviceServiceHandlerInterface):

_url: str
_desired_capabilities: dict
_driver: webdriver
_api_endpoint: str
_lambda_username: str
_lambda_access_key: str

def __init__(self, config_file_path: str):
super().__init__(config_file_path)
self._api_endpoint = 'https://mobile-mgm.lambdatest.com/mfs/v1.0/media/upload'

def set_desired_capabilities(self, config: dict = None):
"""set extra capabilities above what was in the config file"""

# Handle posiible special options
if 'name' in config:
self._CONFIG['capabilities']['lt:options']['name'] = config['name']
config.pop('name')

# Handle common options and capabilities
for item in config:
self._CONFIG['capabilities'][item] = config[item]
#self._CONFIG['capabilities']['fullReset'] = True
self._desired_capabilities = self._CONFIG['capabilities']
print("\n\nDesired Capabilities passed to Appium:")
print(json.dumps(self._desired_capabilities, indent=4))

def set_device_service_specific_options(self, options: dict = None, command_executor_url: str = None):
"""set any specific device options before initialize_driver is called """
"""Order of presededence is options parameter, then the config.json file, then environment variables"""
# if username, access key, and region are not in the config, check the environment.
if options and 'LAMBDA_USERNAME' in options:
self._lambda_username = options['LAMBDA_USERNAME']
else:
if 'LAMBDA_USERNAME' in self._CONFIG:
self._lambda_username = self._CONFIG['LAMBDA_USERNAME']
else:
self._lambda_username = config('LAMBDA_USERNAME')

if options and 'LAMBDA_ACCESS_KEY' in options:
self._lambda_access_key = options['LAMBDA_ACCESS_KEY']
else:
if 'LAMBDA_ACCESS_KEY' in self._CONFIG:
self._lambda_access_key = self._CONFIG['LAMBDA_ACCESS_KEY']
else:
self._lambda_access_key = config('LAMBDA_ACCESS_KEY')

if command_executor_url == None:
self._url = 'http://%s:%[email protected]/wd/hub' % (
self._lambda_username, self._lambda_access_key)
else:
self._url = command_executor_url

# print(url)

async def save_qr_code_to_lambda_test(self, data):
async with aiohttp.ClientSession() as session:
async with session.post(self._api_endpoint, auth=aiohttp.BasicAuth(self._lambda_username, self._lambda_access_key), data=data) as resp:
result = await resp.json()
return result['media_url']



def inject_qrcode(self, image):
"""save qrcode image to the device in lambda test in a way that allows for the device to scan it when the camera opens"""
# get current directory
with open(os.path.dirname(__file__) + '/../qrcode.png', 'rb') as img:
print(img)
payload = {
'media_file': img,
'type': 'image',
'custom_id': 'QRCodeImage'
}
image_url = asyncio.run(self.save_qr_code_to_lambda_test(payload))
self._driver.execute_script(f"lambda-image-injection={image_url}")

def biometrics_authenticate(self, authenticate:bool):
"""authenticate when biometrics, ie fingerprint or faceid, true is success, false is fail biometrics"""
return authenticate

def supports_test_result(self) -> bool:
"""return true if the device service supports setting a pass or fail flag in thier platform"""
return True

def set_test_result(self, passed: bool):
"""set the test result on the device platform. True is pass, False is failure"""
if passed == False:
self._driver.execute_script('lambda-status=failed')
elif passed == True:
self._driver.execute_script('lambda-status=passed')
62 changes: 34 additions & 28 deletions aries-mobile-tests/features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

# Get teh Device Cloud Service passed in from manage
device_cloud_service = config('DEVICE_CLOUD')

# Get the Device Platform
device_platform_name = config('DEVICE_PLATFORM')

# Check if there is a config file override. If not, use the default
try:
config_file_path = config('CONFIG_FILE_OVERRIDE')
Expand Down Expand Up @@ -48,7 +52,7 @@ def before_feature(context, feature):
context.verifier = aif.create_verifier_agent_interface(verifier_type, verifier_endpoint)
context.print_page_source_on_failure = eval(context.config.userdata['print_page_source_on_failure'])
context.print_qr_code_on_creation = eval(context.config.userdata['print_qr_code_on_creation'])
context.save_qr_code_on_creation = eval(context.config.userdata['save_qr_code_on_creation'])
context.save_qr_code_on_creation = True if device_cloud_service == 'LocalAndroid' or device_cloud_service == 'LambdaTest' else eval(context.config.userdata['save_qr_code_on_creation'])

# retry failed tests
try:
Expand Down Expand Up @@ -92,33 +96,35 @@ def after_scenario(context, scenario):



# Add the sauce Labs results and video url to the allure results
# Link that requires a sauce labs account and login
testobject_test_report_url = context.driver.capabilities["testobject_test_report_url"]
allure.attach(testobject_test_report_url, "Sauce Labs Report and Video (Login required)")
print(f"Sauce Labs Report and Video (Login required): {testobject_test_report_url}")

# Since every test scenario is a new session with potentially a different device
# write the capabilities info as an attachment to the test scenario to keep track
allure.attach(json.dumps(context.driver.capabilities,indent=4), "Complete Appium/Sauce Labs Test Environment Configuration")

# Link does not require a sauce labs account and login. Token generated.
# # TODO This isn't working. Have contacted Sauce Labs.
# test_id = testobject_test_report_url.rpartition('/')[-1]
# session_id = context.driver.session_id
# key = f"{username}:{access_key}"
# sl_token = hmac.new(key.encode("ascii"), None, md5).hexdigest()
# url = f"{testobject_test_report_url}?auth={sl_token}"
# allure.attach(url, "Public Sauce Labs Report and Video (Login not required) (Nonfunctional at this time)")
# print(f"Public Sauce Labs Report and Video (Login not required): {url} (Nonfunctional at this time)")

# elif device_cloud_service == "something else in the future":

# if context.driver.capabilities['platformName'] == "iOS":
# context.driver.close_app()
# context.driver.launch_app()
# else:
# context.driver.reset()
if device_cloud_service == 'SauceLabs':
# Add the sauce Labs results and video url to the allure results
# Link that requires a sauce labs account and login
testobject_test_report_url = context.driver.capabilities["testobject_test_report_url"]
allure.attach(testobject_test_report_url, "Sauce Labs Report and Video (Login required)")
print(f"Sauce Labs Report and Video (Login required): {testobject_test_report_url}")

# Since every test scenario is a new session with potentially a different device
# write the capabilities info as an attachment to the test scenario to keep track
allure.attach(json.dumps(context.driver.capabilities,indent=4), "Complete Appium/Sauce Labs Test Environment Configuration")

# Link does not require a sauce labs account and login. Token generated.
# # TODO This isn't working. Have contacted Sauce Labs.
# test_id = testobject_test_report_url.rpartition('/')[-1]
# session_id = context.driver.session_id
# key = f"{username}:{access_key}"
# sl_token = hmac.new(key.encode("ascii"), None, md5).hexdigest()
# url = f"{testobject_test_report_url}?auth={sl_token}"
# allure.attach(url, "Public Sauce Labs Report and Video (Login not required) (Nonfunctional at this time)")
# print(f"Public Sauce Labs Report and Video (Login not required): {url} (Nonfunctional at this time)")

# elif device_cloud_service == "LambdaTest":
# TODO

# if context.driver.capabilities['platformName'] == "iOS":
# context.driver.close_app()
# context.driver.launch_app()
# else:
# context.driver.reset()

if hasattr(context, 'driver'):
context.driver.quit()
Expand Down
19 changes: 19 additions & 0 deletions aries-mobile-tests/lt_android_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"capabilities": {
"lt:options": {
"w3c": true,
"deviceName": "Galaxy.*,Pixel.*,Nexus.*",
"platformName": "Android",
"platformVersion": "10",
"build": "QCWallet-XXX",
"name": "Sample Test Android - Python",
"app": "lt://APP1016045801683122334603392",
"autoGrantPermissions": true,
"enableImageInjection": true,
"media": "lt://MEDIA14a54efe064642f9a9cf39ab5d189751",
"isRealMobile": true,
"visual": true,
"video": true
}
}
}
18 changes: 18 additions & 0 deletions aries-mobile-tests/lt_ios_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"capabilities": {
"lt:options": {
"w3c": true,
"deviceName": "iPhone.*",
"platformName": "iOS",
"build": "QCWallet-XXX",
"name": "Sample Test iOS - Python",
"app": "app_url_lt",
"autoGrantPermissions": true,
"enableImageInjection": true,
"media": "lt://MEDIA14a54efe064642f9a9cf39ab5d189751",
"isRealMobile": true,
"visual": true,
"video": true
}
}
}
3 changes: 2 additions & 1 deletion aries-mobile-tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ qrcode[pil]~=6.1
webdriver-manager==3.8.3
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
google-auth-oauthlib
aiohttp
32 changes: 24 additions & 8 deletions manage
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ usage () {
Use -k to specify the access key for the given device cloud service.
Use -p to specify the mobile device platform
"-p iOS" or "-p Android"
use -b to specify the build name (useful for LambdaTest automation)
Use -a to specify the app name or id of the app loaded into the device cloud service
Use -i to specify the issuer to use in the tests in the -i "<issuer_name>;<issuer_endpoint>" format
Use -v to specify the verifier to use in the tests in the -i "<verifier_name>;<verifier_endpoint>" format
Expand Down Expand Up @@ -204,24 +205,36 @@ modifyConfigJson() {
else
cp ./aries-mobile-tests/local_android_config.json ./aries-mobile-tests/config.json
fi
fi
fi

contents="$(jq --arg DEVICE_PLATFORM "${DEVICE_PLATFORM}" '.capabilities |= . + { "platformName": $DEVICE_PLATFORM }' ./aries-mobile-tests/config.json)" && \
echo "${contents}" > ./aries-mobile-tests/config.json

build_name="Wallet-Build $(date +'%m/%d/%y %H:%M:%S')"

if [[ "${DEVICE_CLOUD}" == "SauceLabs" ]]; then
contents="$(jq --arg APP_NAME "storage:filename=${APP_NAME}" '.capabilities |= . + { "app": $APP_NAME }' ./aries-mobile-tests/config.json)" && \
echo "${contents}" > ./aries-mobile-tests/config.json
elif [[ "${DEVICE_CLOUD}" == "LambdaTest" ]]; then
contents="$(jq --arg APP_NAME "${APP_NAME}" --arg BUILD "${BUILD:=$build_name}" '.capabilities."lt:options" |= . + {"app": $APP_NAME, "build": $BUILD }' ./aries-mobile-tests/config.json)" && \
echo "${contents}" > ./aries-mobile-tests/config.json
contents="$(jq 'del(.capabilities.platformName)' ./aries-mobile-tests/config.json)"
echo "${contents}" > ./aries-mobile-tests/config.json
else
contents="$(jq --arg APP_NAME "${APP_NAME}" '.capabilities |= . + { "app": $APP_NAME }' ./aries-mobile-tests/config.json)" && \
echo "${contents}" > ./aries-mobile-tests/config.json
fi

# contents="$(jq '.capabilities |= . + { "appium:platformName": "storage:filename=${APP_NAME}" }' ./aries-mobile-tests/config.json)" && \
# echo "${contents}" > config.json

export DEVICE=$(echo $contents | jq -r '(.capabilities |= . "deviceName")[]')
export OS_VERSION=$(echo $contents | jq -r '(.capabilities |= . "platformVersion")[]')

if [[ "${DEVICE_CLOUD}" == "LambdaTest" ]]; then
export DEVICE=$(echo $contents | jq -r '.capabilities["lt:options"].deviceName')
export OS_VERSION=$(echo $contents | jq -r '.capabilities["lt:options"].platformVersion')
else
export DEVICE=$(echo $contents | jq -r '(.capabilities |= . "deviceName")[]')
export OS_VERSION=$(echo $contents | jq -r '(.capabilities |= . "platformVersion")[]')
fi

#docker cp ./aries-mobile-tests/config.json aries-mobile-test-harness:/aries-mobile-test-harness/aries-mobile-tests/config.json
}
Expand Down Expand Up @@ -252,7 +265,7 @@ writeEnvProperties() {
}

setDockerEnv() {
DOCKER_ENV="-e DEVICE_CLOUD=${DEVICE_CLOUD}"
DOCKER_ENV="-e DEVICE_CLOUD=${DEVICE_CLOUD} -e DEVICE_PLATFORM=${DEVICE_PLATFORM}"
if [[ "${DEVICE_CLOUD}" = "SauceLabs" ]]; then
if ! [ -z "$DEVICE_CLOUD_USERNAME" ]; then
DOCKER_ENV="${DOCKER_ENV} -e SAUCE_USERNAME=${DEVICE_CLOUD_USERNAME}"
Expand Down Expand Up @@ -347,6 +360,7 @@ startHarness(){

runTests() {
runArgs=${@}
echo $runArgs

if [[ "${TAGS}" ]]; then
echo "Tags: ${TAGS}"
Expand Down Expand Up @@ -375,7 +389,7 @@ runTests() {
setDockerEnv

echo "Running with Device Cloud Service ${DEVICE_CLOUD}"
if [[ "${DEVICE_CLOUD}" = "SauceLabs" ]]; then
if [ "${DEVICE_CLOUD}" = "SauceLabs" ] || [ "${DEVICE_CLOUD}" = "LambdaTest" ]; then
# if Region is not set default to us-west-1
# if [[ -z ${SL_REGION} ]]; then
# export SL_REGION="us-west-1"
Expand Down Expand Up @@ -408,8 +422,8 @@ runTests() {
#${terminalEmu} docker run ${INTERACTIVE} --rm --network="host" -v ${APP_NAME}:${APP_NAME} -v "${ANDROID_HOME}/emulator/resources/":"${ANDROID_HOME}/emulator/resources/" -v ${BEHAVE_INI_TMP}:/aries-mobile-tests/behave.ini -v "$(pwd)/aries-mobile-tests/config.json:/aries-mobile-test-harness/aries-mobile-tests/config.json" -e ANDROID_HOME=${ANDROID_HOME} -e DOCKERHOST=${DOCKERHOST} -e DEVICE_CLOUD=${DEVICE_CLOUD} aries-mobile-test-harness -k ${runArgs} -D Issuer=${issuerURL} -D Verifier=${verifierURL}
${terminalEmu} docker run ${INTERACTIVE} --rm --network="host" -v ${APP_NAME}:${APP_NAME} -v "${ANDROID_HOME}/emulator/resources/":"${ANDROID_HOME}/emulator/resources/" -v ${BEHAVE_INI_TMP}:/aries-mobile-tests/behave.ini -v "$(pwd)/aries-mobile-tests/config.json:/aries-mobile-test-harness/aries-mobile-tests/config.json" $DOCKER_ENV aries-mobile-test-harness -k ${runArgs} -D Issuer=${issuerURL} -D Verifier=${verifierURL}
fi
#else # place holder for other device cloud services like BrowserStack, etc.
fi
#else # place holder for other device cloud services like BrowserStack, etc.

local docker_result=$?
rm ${BEHAVE_INI_TMP}
Expand Down Expand Up @@ -542,7 +556,7 @@ if [[ "${COMMAND}" == "run" || "${COMMAND}" == "start" || "${COMMAND}" == "test"
TAGS=""
BEHAVE_INI=aries-mobile-tests/behave.ini

while getopts ":d:u:k:p:a:i:v:r:t:" FLAG; do
while getopts ":d:u:k:p:a:b:i:v:r:t:" FLAG; do
case $FLAG in
h ) usage ;;
: ) usage ;;
Expand All @@ -552,6 +566,8 @@ if [[ "${COMMAND}" == "run" || "${COMMAND}" == "start" || "${COMMAND}" == "test"
;;
r ) export REPORT=${OPTARG}
;;
b ) export BUILD=${OPTARG}
;;
d ) export DEVICE_CLOUD=${OPTARG}
;;
u ) export DEVICE_CLOUD_USERNAME=${OPTARG}
Expand Down

0 comments on commit 4734397

Please sign in to comment.