Skip to content

Commit

Permalink
Device Advisor CI automation (#407)
Browse files Browse the repository at this point in the history
Description of changes:
Add the device advisor scripts to enable GitHub Actions to automatically run device advisor test on push

GitHub Setting Changes:
Added Repository secrets: AWS_DATEST_ACCESS_KEY_ID, AWS_DATEST_SECRET_ACCESS_KEY
The secrets are set to aws-sdk-common-runtime user: IotSDKDeviceAdvisorCIAutomation

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
xiazhvera authored Apr 14, 2022
1 parent 1f06463 commit 732ab98
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 57 deletions.
36 changes: 26 additions & 10 deletions .builder/actions/build_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@

class BuildSamples(Builder.Action):
def run(self, env):
if env.args.cli_config['variables'].get('skip_samples', "0") != "0":
print('skip_samples is defined. Skipping samples...')
return

# parse extra cmake configs
parser = argparse.ArgumentParser()
parser.add_argument('--cmake-extra', action='append', default=[])
Expand All @@ -31,17 +27,37 @@ def run(self, env):
'samples/secure_tunneling/secure_tunnel',
'samples/secure_tunneling/tunnel_notification',
]
da_samples = [
'deviceadvisor/tests/mqtt_connect',
'deviceadvisor/tests/mqtt_publish',
'deviceadvisor/tests/mqtt_subscribe',
'deviceadvisor/tests/shadow_update'
]

for sample_path in samples:
build_path = os.path.join('build', sample_path)
steps.append(['cmake',
f'-B{build_path}',
f'-H{sample_path}',
f'-DCMAKE_PREFIX_PATH={env.install_dir}',
'-DCMAKE_BUILD_TYPE=RelWithDebInfo'])
f'-B{build_path}',
f'-H{sample_path}',
f'-DCMAKE_PREFIX_PATH={env.install_dir}',
'-DCMAKE_BUILD_TYPE=RelWithDebInfo'])
# append extra cmake configs
steps[-1].extend(cmd_args.cmake_extra)
steps.append(['cmake',
'--build', build_path,
'--config', 'RelWithDebInfo'])

for sample_path in da_samples:
build_path = os.path.join('build', sample_path)
steps.append(['cmake',
f'-B{build_path}',
f'-H{sample_path}',
f'-DCMAKE_PREFIX_PATH={env.install_dir}',
'-DCMAKE_BUILD_TYPE=RelWithDebInfo'])
# append extra cmake configs
steps[-1].extend(cmd_args.cmake_extra)
steps.append(['cmake',
'--build', build_path,
'--config', 'RelWithDebInfo'])
'--build', build_path,
'--config', 'RelWithDebInfo'])

return Builder.Script(steps)
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ env:
PACKAGE_NAME: aws-iot-device-sdk-cpp-v2
LINUX_BASE_IMAGE: ubuntu-16-x64
RUN: ${{ github.run_id }}-${{ github.run_number }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DATEST_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DATEST_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1

jobs:
linux-compat:
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Build ${{ env.PACKAGE_NAME }}
run: |
aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBYO_CRYPTO=ON skip_samples=1
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --cmake-extra=-DBYO_CRYPTO=ON --variant=skip_sample
linux-no-cpu-extensions:
runs-on: ubuntu-latest
Expand Down
19 changes: 18 additions & 1 deletion builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"search_dirs": [
"crt"
],
"env": {
"DA_TOPIC": "test/da",
"DA_SHADOW_PROPERTY": "datest",
"DA_SHADOW_VALUE_SET": "ON",
"DA_SHADOW_VALUE_DEFAULT": "OFF"
},
"hosts": {
"manylinux": {
"architectures": {
Expand All @@ -20,5 +26,16 @@
"build_steps": [
"build",
"build-samples"
]
],
"test_steps": [
"python3 -m pip install boto3",
"python3 deviceadvisor/script/DATestRun.py"],
"variants" : {
"skip_sample": {
"!test_steps": [],
"!build_steps": [
"build"
]
}
}
}
19 changes: 19 additions & 0 deletions deviceadvisor/script/DATestConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"tests" :[ "MQTT Connect", "MQTT Publish", "MQTT Subscribe", "Shadow Publish", "Shadow Update"],
"test_suite_ids" :
{
"MQTT Connect" : "ejbdzmo3hf3v",
"MQTT Publish" : "euw7favf6an4",
"MQTT Subscribe" : "01o8vo6no7sd",
"Shadow Publish" : "elztm2jebc1q",
"Shadow Update" : "vuydgrbbbfce"
},
"test_exe_path" :
{
"MQTT Connect" : "mqtt_connect",
"MQTT Publish" : "mqtt_publish",
"MQTT Subscribe" : "mqtt_subscribe",
"Shadow Publish" : "shadow_update",
"Shadow Update" : "shadow_update"
}
}
224 changes: 224 additions & 0 deletions deviceadvisor/script/DATestRun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import boto3
import uuid
import json
import os
import subprocess
import platform
from time import sleep

##############################################
# Cleanup Certificates and Things and created certificate and private key file
def delete_thing_with_certi(thingName, certiId, certiArn):
client.detach_thing_principal(
thingName = thingName,
principal = certiArn)
client.update_certificate(
certificateId =certiId,
newStatus ='INACTIVE')
client.delete_certificate(certificateId = certiId, forceDelete = True)
client.delete_thing(thingName = thingName)
os.remove(os.environ["DA_CERTI"])
os.remove(os.environ["DA_KEY"])


##############################################
# Initialize variables
# create aws clients
client = boto3.client('iot')
dataClient = boto3.client('iot-data')
deviceAdvisor = boto3.client('iotdeviceadvisor')

# load test config
f = open('deviceadvisor/script/DATestConfig.json')
DATestConfig = json.load(f)
f.close()

# create an temporary certificate/key file path
certificate_path = os.path.join(os.getcwd(), 'certificate.pem.crt')
key_path = os.path.join(os.getcwd(), 'private.pem.key')

# load environment variables requried for testing
shadowProperty = os.environ['DA_SHADOW_PROPERTY']
shadowDefault = os.environ['DA_SHADOW_VALUE_DEFAULT']

# test result
test_result = {}

for test_name in DATestConfig['tests']:
##############################################
# create a test thing
thing_name = "DATest_" + str(uuid.uuid4())
try:
# create_thing_response:
# {
# 'thingName': 'string',
# 'thingArn': 'string',
# 'thingId': 'string'
# }
print("[Device Advisor]Info: Started to create thing...")
create_thing_response = client.create_thing(
thingName=thing_name
)
os.environ["DA_THING_NAME"] = thing_name

except Exception as e:
print("[Device Advisor]Error: Failed to create thing: " + thing_name)
exit(-1)


##############################################
# create certificate and keys used for testing
try:
print("[Device Advisor]Info: Started to create certificate...")
# create_cert_response:
# {
# 'certificateArn': 'string',
# 'certificateId': 'string',
# 'certificatePem': 'string',
# 'keyPair':
# {
# 'PublicKey': 'string',
# 'PrivateKey': 'string'
# }
# }
create_cert_response = client.create_keys_and_certificate(
setAsActive=True
)
# write certificate to file
f = open(certificate_path, "w")
f.write(create_cert_response['certificatePem'])
f.close()

# write private key to file
f = open(key_path, "w")
f.write(create_cert_response['keyPair']['PrivateKey'])
f.close()

# setup environment variable
os.environ["DA_CERTI"] = certificate_path
os.environ["DA_KEY"] = key_path

except:
client.delete_thing(thingName = thing_name)
print("[Device Advisor]Error: Failed to create certificate.")
exit(-1)

##############################################
# attach certification to thing
try:
print("[Device Advisor]Info: Attach certificate to test thing...")
# attache the certificate to thing
client.attach_thing_principal(
thingName = thing_name,
principal = create_cert_response['certificateArn']
)

certificate_arn = create_cert_response['certificateArn']
certificate_id = create_cert_response['certificateId']

except:
delete_thing_with_certi(thing_name, certificate_id ,certificate_arn )
print("[Device Advisor]Error: Failed to attach certificate.")
exit(-1)


##############################################
# Run device advisor
try:
######################################
# set default shadow, for shadow update, if the
# shadow does not exists, update will fail
payload_shadow = json.dumps(
{
"state": {
"desired": {
shadowProperty: shadowDefault
},
"reported": {
shadowProperty: shadowDefault
}
}
})
shadow_response = dataClient.update_thing_shadow(
thingName = thing_name,
payload = payload_shadow)
get_shadow_response = dataClient.get_thing_shadow(thingName = thing_name)
# make sure shadow is created before we go to next step
while(get_shadow_response is None):
get_shadow_response = dataClient.get_thing_shadow(thingName = thing_name)

# start device advisor test
# test_start_response
# {
# 'suiteRunId': 'string',
# 'suiteRunArn': 'string',
# 'createdAt': datetime(2015, 1, 1)
# }
print("[Device Advisor]Info: Start device advisor test: " + test_name)
test_start_response = deviceAdvisor.start_suite_run(
suiteDefinitionId=DATestConfig['test_suite_ids'][test_name],
suiteRunConfiguration={
'primaryDevice': {
'thingArn': create_thing_response['thingArn'],
},
'parallelRun': True
})

# get DA endpoint
endpoint_response = deviceAdvisor.get_endpoint(
thingArn = create_thing_response['thingArn']
)
os.environ['DA_ENDPOINT'] = endpoint_response['endpoint']

while True:
# sleep for 1s every loop to avoid TooManyRequestsException
sleep(1)
test_result_responds = deviceAdvisor.get_suite_run(
suiteDefinitionId=DATestConfig['test_suite_ids'][test_name],
suiteRunId=test_start_response['suiteRunId']
)
# If the status is PENDING or the responds does not loaded, the test suite is still loading
if (test_result_responds['status'] == 'PENDING' or
len(test_result_responds['testResult']['groups']) == 0 or # test group has not been loaded
len(test_result_responds['testResult']['groups'][0]['tests']) == 0 or #test case has not been loaded
test_result_responds['testResult']['groups'][0]['tests'][0]['status'] == 'PENDING'):
continue

# Start to run the test sample after the status turns into RUNNING
elif (test_result_responds['status'] == 'RUNNING' and
test_result_responds['testResult']['groups'][0]['tests'][0]['status'] == 'RUNNING'):
try:
exe_path = os.path.join("build/deviceadvisor/tests/",DATestConfig['test_exe_path'][test_name])
# Windows and MAC/LINUX has a different build folder structure
if platform.system() == 'Windows':
exe_path = os.path.join(exe_path, "RelWithDebInfo",DATestConfig['test_exe_path'][test_name])
else:
exe_path = os.path.join(exe_path, DATestConfig['test_exe_path'][test_name])
print("start to run" + exe_path)
result = subprocess.run(exe_path, timeout = 60*2)
except:
continue

# If the test finalizing or store the test result
elif (test_result_responds['status'] != 'RUNNING'):
test_result[test_name] = test_result_responds['status']
if(test_result[test_name] == "PASS"):
delete_thing_with_certi(thing_name, certificate_id ,certificate_arn )
break
except Exception as e:
print("[Device Advisor]Error: Failed to test: "+ test_name)

##############################################
# print result and cleanup things
print(test_result)
failed = False
for test in test_result:
if(test_result[test] != "PASS" and
test_result[test] != "PASS_WITH_WARNINGS"):
print("[Device Advisor]Error: Test \"" + test + "\" Failed with status:" + test_result[test])
failed = True
if failed:
# if the test failed, we dont clean the Thing so that we can track the error
exit(-1)

exit(0)
1 change: 1 addition & 0 deletions deviceadvisor/tests/mqtt_connect/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ project(mqtt_connect CXX)
file(GLOB SRC_FILES
"*.cpp"
"../utils/*.cpp"
"../utils/*.h"
)

add_executable(${PROJECT_NAME} ${SRC_FILES})
Expand Down
5 changes: 2 additions & 3 deletions deviceadvisor/tests/mqtt_connect/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ int main()
* Do the global initialization for the API.
*/
ApiHandle apiHandle;

String clientId(String("test-") + Aws::Crt::UUID().ToString());

/*********************** Parse Arguments ***************************/
Expand All @@ -37,7 +36,6 @@ int main()
/*
* Setup client configuration with the MqttClientConnectionConfigBuilder.
*/

Aws::Iot::MqttClientConnectionConfigBuilder builder =
Aws::Iot::MqttClientConnectionConfigBuilder(daVars.certificatePath.c_str(), daVars.keyPath.c_str());
builder.WithEndpoint(daVars.endpoint);
Expand Down Expand Up @@ -79,7 +77,8 @@ int main()
/*
* Actually perform the connect dance.
*/
if (!connection->Connect(clientId.c_str(), false /*cleanSession*/, 1000 /*keepAliveTimeSecs*/))
if (!connection->Connect(
clientId.c_str(), false /*cleanSession*/, 1000 /*keepAliveTimeSecs*/, 6000 /*pingTimeoutMs*/))
{
exit(-1);
}
Expand Down
1 change: 1 addition & 0 deletions deviceadvisor/tests/mqtt_publish/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ project(mqtt_publish CXX)
file(GLOB SRC_FILES
"*.cpp"
"../utils/*.cpp"
"../utils/*.h"
)

add_executable(${PROJECT_NAME} ${SRC_FILES})
Expand Down
Loading

0 comments on commit 732ab98

Please sign in to comment.