Skip to content

Commit

Permalink
Device Advisor CI automation (#232)
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 745d78d commit 4b150d2
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 32 deletions.
46 changes: 42 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ env:
BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net
PACKAGE_NAME: aws-iot-device-sdk-java-v2
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_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
# TEMP AWS KEY FOR TESTING
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
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
DA_TOPIC: test/da
DA_SHADOW_PROPERTY: datest
DA_SHADOW_VALUE_SET: ON
DA_SHADOW_VALUE_DEFAULT: OFF

jobs:
linux-compat:
Expand All @@ -36,15 +44,45 @@ jobs:
windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
version:
- 8
- 11
- 17
steps:
- name: Checkout Sources
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: ${{ matrix.version }}
cache: maven
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
python -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder.pyz')"
python builder.pyz build -p ${{ env.PACKAGE_NAME }} --spec=downstream
osx:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
version:
- 8
- 11
- 17
steps:
- name: Checkout Sources
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: ${{ matrix.version }}
cache: maven
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')"
Expand Down Expand Up @@ -72,4 +110,4 @@ jobs:
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
java -version
mvn -B test
mvn -B test
16 changes: 14 additions & 2 deletions builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@
"mvn -B compile"
],
"test_steps": [
"mvn -B test"
"python3 -m pip install boto3",
"python3 deviceadvisor/script/DATestRun.py"
],
"imports": [
"JDK8"
],
"env": {
"DA_TOPIC": "test/da",
"DA_SHADOW_PROPERTY": "datest",
"DA_SHADOW_VALUE_SET": "ON",
"DA_SHADOW_VALUE_DEFAULT": "OFF"
},
"hosts": {
"ubuntu": {
"packages": [
Expand All @@ -23,5 +30,10 @@
"openjdk-8-jdk-headless"
]
}
}
},
"variants" : {
"skip_test": {
"!test_steps": []
}
}
}
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" : "MQTTConnect",
"MQTT Publish" : "MQTTPublish",
"MQTT Subscribe" : "MQTTSubscribe",
"Shadow Publish" : "ShadowUpdate",
"Shadow Update" : "ShadowUpdate"
}
}
226 changes: 226 additions & 0 deletions deviceadvisor/script/DATestRun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
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']

##############################################
# make sure sdk get installed
print("[Device Advisor]Info: Start to build sdk...")
subprocess.run("mvn clean install", shell = True)

# 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'):
working_dir = os.getcwd()
exe_path = os.path.join("deviceadvisor/tests/",DATestConfig['test_exe_path'][test_name])
os.chdir(exe_path)
print(os.getcwd())
run_cmd = 'mvn clean compile exec:java -Dexec.mainClass='+DATestConfig['test_exe_path'][test_name] + '.' + DATestConfig['test_exe_path'][test_name]
print("run_cmd:" + run_cmd)
result = subprocess.run(run_cmd, shell = True, timeout= 60*2)
# mvn compile exec:java -pl deviceadvisor/tests/MQTTConnect -Dexec.mainClass=MQTTConnect.MQTTConnect
# mvn exec:java -Dexec.mainClass="com.example.Main"
os.chdir(working_dir)
# 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)
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ public static void main(String[] args) {
}

try(AwsIotMqttConnectionBuilder builder = AwsIotMqttConnectionBuilder.newMtlsBuilderFromPath(DATestUtils.certificatePath, DATestUtils.keyPath)) {

builder.withClientId(clientId)
.withEndpoint(DATestUtils.endpoint)
.withPort((short)port)
.withCleanSession(true)
.withPingTimeoutMs(60000)
.withProtocolOperationTimeoutMs(60000);

try(MqttClientConnection connection = builder.build()) {

CompletableFuture<Boolean> connected = connection.connect();
try {
boolean sessionPresent = connected.get();
Expand All @@ -67,9 +65,9 @@ public static void main(String[] args) {
disconnected.get();
}
} catch (CrtRuntimeException | InterruptedException | ExecutionException ex) {

System.out.print("failed: " + ex.getMessage());
}

CrtResource.waitForNoResources();
System.exit(0);
}
}
Loading

0 comments on commit 4b150d2

Please sign in to comment.