Skip to content

Commit e297944

Browse files
authored
Shadow Update Service Test (#539)
* Shadow Update Service Test * Add command input * Change shadow file name * Fix executable path * Add mqtt version number * Fix named shadow path * Add shadow name parameter * Add Named Shadow support * Debug: don't delete thing * create named shadow from script * create shadow * Add mqtt5 support * removes unsused code * Remove dead code * Fix comments
1 parent 1d3e206 commit e297944

File tree

9 files changed

+793
-3
lines changed

9 files changed

+793
-3
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ env:
3434
CI_BUILD_AND_TEST_ROLE: arn:aws:iam::180635532705:role/V2_SDK_Unit_Testing
3535
CI_JOBS_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_JobsServiceClient_Role
3636
CI_SERVICE_ROLE_CFG_FOLDER: "./aws-iot-device-sdk-python-v2/servicetests/test_cases"
37+
CI_SHADOW_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_ShadowServiceClient_Role
3738

3839
jobs:
3940

@@ -207,6 +208,32 @@ jobs:
207208
chmod a+x builder
208209
./builder build -p ${{ env.PACKAGE_NAME }}
209210
211+
- name: configure AWS credentials (service tests Shadow)
212+
uses: aws-actions/configure-aws-credentials@v2
213+
with:
214+
role-to-assume: ${{ env.CI_SHADOW_SERVICE_CLIENT_ROLE }}
215+
aws-region: ${{ env.AWS_DEFAULT_REGION }}
216+
- name: run MQTT5 Shadow Update
217+
working-directory: ./aws-iot-device-sdk-python-v2/servicetests
218+
run: |
219+
export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples
220+
python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_shadow_cfg.json
221+
- name: run MQTT3 Shadow Update
222+
working-directory: ./aws-iot-device-sdk-python-v2/servicetests
223+
run: |
224+
export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples
225+
python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_shadow_cfg.json
226+
- name: run MQTT5 Named Shadow Update
227+
working-directory: ./aws-iot-device-sdk-python-v2/servicetests
228+
run: |
229+
export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples
230+
python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_named_shadow_cfg.json
231+
- name: run MQTT3 Named Shadow Update
232+
working-directory: ./aws-iot-device-sdk-python-v2/servicetests
233+
run: |
234+
export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-python-v2/utils:${{ github.workspace }}/aws-iot-device-sdk-python-v2/samples
235+
python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_named_shadow_cfg.json
236+
210237
- name: configure AWS credentials (service tests Jobs)
211238
uses: aws-actions/configure-aws-credentials@v2
212239
with:

samples/utils/command_line_utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ class CmdData:
289289
input_job_time : int
290290
# Shadow
291291
input_shadow_property : str
292+
input_shadow_value : str
293+
input_shadow_name : str
292294
# PKCS12
293295
input_pkcs12_file : str
294296
input_pkcs12_password : str
@@ -703,7 +705,10 @@ def parse_sample_input_shadow():
703705
cmdUtils.register_command(CommandLineUtils.m_cmd_port, "<int>", "Connection port. AWS IoT supports 443 and 8883 (optional, default=8883).", type=int)
704706
cmdUtils.register_command(CommandLineUtils.m_cmd_client_id, "<str>", "Client ID to use for MQTT connection (optional, default='test-*').", default="test-" + str(uuid4()))
705707
cmdUtils.register_command(CommandLineUtils.m_cmd_thing_name, "<str>", "The name assigned to your IoT Thing", required=True)
706-
cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_property, "<str>", "The name of the shadow property you want to change (optional, default='color'", default="color")
708+
cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_property, "<str>", "The name of the shadow property you want to change (optional, default=''", default="")
709+
cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_value, "<str>", "The desired value of the shadow property you want to set (optional)")
710+
cmdUtils.register_command(CommandLineUtils.m_cmd_shadow_name, "<str>", "Shadow name (optional, default='')", type=str)
711+
cmdUtils.register_command(CommandLineUtils.m_cmd_mqtt_version, "<int>", "mqtt version (optional, default='5')", default=5, type=int)
707712
cmdUtils.get_args()
708713

709714
cmdData = CommandLineUtils.CmdData()
@@ -717,7 +722,10 @@ def parse_sample_input_shadow():
717722
cmdData.input_proxy_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_port))
718723
cmdData.input_thing_name = cmdUtils.get_command_required(CommandLineUtils.m_cmd_thing_name)
719724
cmdData.input_shadow_property = cmdUtils.get_command_required(CommandLineUtils.m_cmd_shadow_property)
725+
cmdData.input_shadow_value = cmdUtils.get_command(CommandLineUtils.m_cmd_shadow_value, None)
726+
cmdData.input_shadow_name = cmdUtils.get_command(CommandLineUtils.m_cmd_shadow_name, None)
720727
cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None
728+
cmdData.input_mqtt_version = int(cmdUtils.get_command(CommandLineUtils.m_cmd_mqtt_version, 5))
721729
return cmdData
722730

723731
def parse_sample_input_websocket_connect():
@@ -876,6 +884,8 @@ def parse_sample_input_pkcs12_connect():
876884
m_cmd_count = "count"
877885
m_cmd_group_identifier = "group_identifier"
878886
m_cmd_shadow_property = "shadow_property"
887+
m_cmd_shadow_value = "shadow_value"
888+
m_cmd_shadow_name = "shadow_name"
879889
m_cmd_pkcs12_file = "pkcs12_file"
880890
m_cmd_pkcs12_password = "pkcs12_password"
881891
m_cmd_region = "region"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"language": "Python",
3+
"runnable_file": "./tests/ShadowUpdate/shadow_update.py",
4+
"runnable_region": "us-east-1",
5+
"runnable_main_class": "",
6+
"arguments": [
7+
{
8+
"name": "--mqtt_version",
9+
"data": "3"
10+
},
11+
{
12+
"name": "--endpoint",
13+
"secret": "ci/endpoint"
14+
},
15+
{
16+
"name": "--cert",
17+
"data": "tests/ShadowUpdate/certificate.pem.crt"
18+
},
19+
{
20+
"name": "--key",
21+
"data": "tests/ShadowUpdate/private.pem.key"
22+
},
23+
{
24+
"name": "--thing_name",
25+
"data": "ServiceTest_Shadow_$INPUT_UUID"
26+
},
27+
{
28+
"name": "--shadow_property",
29+
"data": "color"
30+
},
31+
{
32+
"name": "--shadow_value",
33+
"data": "on"
34+
},
35+
{
36+
"name": "--shadow_name",
37+
"data": "testShadow"
38+
}
39+
]
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"language": "Python",
3+
"runnable_file": "./tests/ShadowUpdate/shadow_update.py",
4+
"runnable_region": "us-east-1",
5+
"runnable_main_class": "",
6+
"arguments": [
7+
{
8+
"name": "--mqtt_version",
9+
"data": "3"
10+
},
11+
{
12+
"name": "--endpoint",
13+
"secret": "ci/endpoint"
14+
},
15+
{
16+
"name": "--cert",
17+
"data": "tests/ShadowUpdate/certificate.pem.crt"
18+
},
19+
{
20+
"name": "--key",
21+
"data": "tests/ShadowUpdate/private.pem.key"
22+
},
23+
{
24+
"name": "--thing_name",
25+
"data": "ServiceTest_Shadow_$INPUT_UUID"
26+
},
27+
{
28+
"name": "--shadow_property",
29+
"data": "color"
30+
},
31+
{
32+
"name": "--shadow_value",
33+
"data": "on"
34+
}
35+
]
36+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"language": "Python",
3+
"runnable_file": "./tests/ShadowUpdate/shadow_update.py",
4+
"runnable_region": "us-east-1",
5+
"runnable_main_class": "",
6+
"arguments": [
7+
{
8+
"name": "--mqtt_version",
9+
"data": "5"
10+
},
11+
{
12+
"name": "--endpoint",
13+
"secret": "ci/endpoint"
14+
},
15+
{
16+
"name": "--cert",
17+
"data": "tests/ShadowUpdate/certificate.pem.crt"
18+
},
19+
{
20+
"name": "--key",
21+
"data": "tests/ShadowUpdate/private.pem.key"
22+
},
23+
{
24+
"name": "--thing_name",
25+
"data": "ServiceTest_Shadow_$INPUT_UUID"
26+
},
27+
{
28+
"name": "--shadow_property",
29+
"data": "color"
30+
},
31+
{
32+
"name": "--shadow_value",
33+
"data": "on"
34+
},
35+
{
36+
"name": "--shadow_name",
37+
"data": "testShadow"
38+
}
39+
]
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"language": "Python",
3+
"runnable_file": "./tests/ShadowUpdate/shadow_update.py",
4+
"runnable_region": "us-east-1",
5+
"runnable_main_class": "",
6+
"arguments": [
7+
{
8+
"name": "--mqtt_version",
9+
"data": "5"
10+
},
11+
{
12+
"name": "--endpoint",
13+
"secret": "ci/endpoint"
14+
},
15+
{
16+
"name": "--cert",
17+
"data": "tests/ShadowUpdate/certificate.pem.crt"
18+
},
19+
{
20+
"name": "--key",
21+
"data": "tests/ShadowUpdate/private.pem.key"
22+
},
23+
{
24+
"name": "--thing_name",
25+
"data": "ServiceTest_Shadow_$INPUT_UUID"
26+
},
27+
{
28+
"name": "--shadow_property",
29+
"data": "color"
30+
},
31+
{
32+
"name": "--shadow_value",
33+
"data": "on"
34+
}
35+
]
36+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-3.0.
3+
4+
import argparse
5+
import json
6+
import os
7+
import sys
8+
import uuid
9+
import time
10+
11+
import boto3
12+
13+
import run_in_ci
14+
import ci_iot_thing
15+
16+
17+
def get_shadow_attrs(config_file):
18+
with open(config_file) as f:
19+
json_data = json.load(f)
20+
shadow_name = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_name"), "")
21+
shadow_property = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_property"), "")
22+
shadow_desired_value = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_value"), "")
23+
return [shadow_name, shadow_property, shadow_desired_value]
24+
25+
26+
def main():
27+
argument_parser = argparse.ArgumentParser(
28+
description="Run Shadow test in CI")
29+
argument_parser.add_argument(
30+
"--config-file", required=True,
31+
help="JSON file providing command-line arguments for a test")
32+
argument_parser.add_argument(
33+
"--input-uuid", required=False, help="UUID for thing name. UUID will be generated if this option is omit")
34+
argument_parser.add_argument(
35+
"--region", required=False, default="us-east-1", help="The name of the region to use")
36+
parsed_commands = argument_parser.parse_args()
37+
38+
[shadow_name, shadow_property, shadow_desired_value] = get_shadow_attrs(parsed_commands.config_file)
39+
print(f"Shadow name: '{shadow_name}'")
40+
print(f"Shadow property: '{shadow_property}'")
41+
print(f"Shadow desired value: '{shadow_desired_value}'")
42+
43+
try:
44+
iot_data_client = boto3.client('iot-data', region_name=parsed_commands.region)
45+
secrets_client = boto3.client("secretsmanager", region_name=parsed_commands.region)
46+
except Exception as e:
47+
print(f"ERROR: Could not make Boto3 iot-data client. Credentials likely could not be sourced. Exception: {e}",
48+
file=sys.stderr)
49+
return -1
50+
51+
input_uuid = parsed_commands.input_uuid if parsed_commands.input_uuid else str(uuid.uuid4())
52+
53+
thing_name = "ServiceTest_Shadow_" + input_uuid
54+
policy_name = secrets_client.get_secret_value(
55+
SecretId="ci/ShadowServiceClientTest/policy_name")["SecretString"]
56+
57+
# Temporary certificate/key file path.
58+
certificate_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/certificate.pem.crt")
59+
key_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/private.pem.key")
60+
61+
try:
62+
ci_iot_thing.create_iot_thing(
63+
thing_name=thing_name,
64+
region=parsed_commands.region,
65+
policy_name=policy_name,
66+
certificate_path=certificate_path,
67+
key_path=key_path)
68+
except Exception as e:
69+
print(f"ERROR: Failed to create IoT thing: {e}")
70+
sys.exit(-1)
71+
72+
# Perform Shadow test. If it's successful, a shadow should appear for a specified thing.
73+
try:
74+
test_result = run_in_ci.setup_and_launch(parsed_commands.config_file, input_uuid)
75+
except Exception as e:
76+
print(f"ERROR: Failed to create shadow test: {e}")
77+
test_result = -1
78+
79+
# Test reported success, verify that shadow was indeed updated.
80+
if test_result == 0:
81+
print("Verifying that shadow was updated")
82+
shadow_value = None
83+
try:
84+
if shadow_name:
85+
thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name, shadowName=shadow_name)
86+
else:
87+
thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name)
88+
89+
payload = thing_shadow['payload'].read()
90+
data = json.loads(payload)
91+
shadow_value = data.get('state', {}).get('reported', {}).get(shadow_property, None)
92+
if shadow_value != shadow_desired_value:
93+
print(f"ERROR: Could not verify thing shadow: {shadow_property} is not set to desired value "
94+
f"'{shadow_desired_value}'; shadow actual state: {data}")
95+
test_result = -1
96+
except KeyError as e:
97+
print(f"ERROR: Could not verify thing shadow: key {e} does not exist in shadow response: {thing_shadow}")
98+
test_result = -1
99+
except Exception as e:
100+
print(f"ERROR: Could not verify thing shadow: {e}")
101+
test_result = -1
102+
103+
if test_result == 0:
104+
print("Test succeeded")
105+
106+
# Delete a thing created for this test run.
107+
# NOTE We want to try to delete thing even if test was unsuccessful.
108+
try:
109+
ci_iot_thing.delete_iot_thing(thing_name, parsed_commands.region)
110+
except Exception as e:
111+
print(f"ERROR: Failed to delete thing: {e}")
112+
# Fail the test if unable to delete thing, so this won't remain unnoticed.
113+
test_result = -1
114+
115+
try:
116+
if os.path.isfile(certificate_path):
117+
os.remove(certificate_path)
118+
if os.path.isfile(key_path):
119+
os.remove(key_path)
120+
except Exception as e:
121+
print(f"WARNING: Failed to delete local files: {e}")
122+
123+
if test_result != 0:
124+
sys.exit(-1)
125+
126+
127+
if __name__ == "__main__":
128+
main()

servicetests/tests/JobsExecution/jobs.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ def exit(msg_or_exception):
7676
print("Disconnecting...")
7777
locked_data.disconnect_called = True
7878
if cmdData.input_mqtt_version == 5:
79-
locked_data.disconnect_called = True
8079
mqtt5_client.stop()
8180
else:
8281
future = mqtt_connection.disconnect()
@@ -335,7 +334,6 @@ def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
335334
print("Unsopported MQTT version number\n")
336335
sys.exit(-1)
337336

338-
339337
print("Connected!")
340338

341339
try:

0 commit comments

Comments
 (0)