Skip to content

Commit 9fd326f

Browse files
committed
actions: Add board provisioning testing
Run provisioning tests on schedule. Signed-off-by: Jorgen Kvalvaag <[email protected]>
1 parent 55dd378 commit 9fd326f

5 files changed

Lines changed: 111 additions & 48 deletions

File tree

.github/workflows/build-and-target-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
PUSH=${{ github.event_name == 'push' }}
3737
push_memory_badges=false
3838
if [[ $SCHEDULED == true ]]; then
39-
devices='["thingy91x","nrf9151dk","ppk_thingy91x","gnss_nrf9151dk"]'
39+
devices='["thingy91x","nrf9151dk","ppk_thingy91x","gnss_nrf9151dk", "prov_thingy91x"]'
4040
build_all=true
4141
push_memory_badges=true
4242
elif [[ $PUSH == true ]]; then

app/prj.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,5 @@ CONFIG_NRF_PROVISIONING_RX_BUF_SZ=4096
244244
CONFIG_NRF_PROVISIONING_TX_BUF_SZ=4096
245245
CONFIG_NRF_PROVISIONING_CODEC_AT_CMD_LEN=2048
246246
CONFIG_NRF_PROVISIONING_CODEC_RX_SZ_START=2048
247+
248+
CONFIG_NRF_PROVISIONING_COAP_TIMEOUT_SECONDS=6

tests/on_target/tests/conftest.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@
2323
DEVICE_UUID = os.getenv('UUID')
2424
NRFCLOUD_API_KEY = os.getenv('NRFCLOUD_API_KEY')
2525
DUT_DEVICE_TYPE = os.getenv('DUT_DEVICE_TYPE')
26-
if DUT_DEVICE_TYPE == "ppk_thingy91x":
27-
DUT_DEVICE_TYPE = "thingy91x"
28-
2926

3027
def get_uarts():
3128
base_path = "/dev/serial/by-id"

tests/on_target/tests/test_provisioning/test_provisioning.py

Lines changed: 105 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22
import sys
33
import json
4-
import requests.exceptions # Used for handling HTTP errors from nRF Cloud API
4+
import time
5+
import pytest
6+
import requests.exceptions # Used for handling HTTP errors from nRF Cloud API
57

68
# Ensure the utils directory is in the Python path
79
sys.path.append(os.getcwd())
@@ -16,6 +18,7 @@
1618

1719
# --- Helper Functions ---
1820

21+
1922
def _perform_initial_device_setup_and_factory_reset(dut_cloud, hex_file: str):
2023
logger.info(f"Flashing device with {hex_file} and performing factory reset.")
2124

@@ -24,40 +27,57 @@ def _perform_initial_device_setup_and_factory_reset(dut_cloud, hex_file: str):
2427
dut_cloud.uart.flush()
2528
reset_device()
2629

30+
2731
def _wait_for_lte_connection(dut_cloud, timeout: int = 240):
2832
logger.info("Waiting for device to connect to LTE network...")
2933

3034
log_pattern_network_connected = "network: Network connectivity established"
31-
dut_cloud.uart.wait_for_str(log_pattern_network_connected, timeout=timeout)
35+
dut_cloud.uart.wait_for_str(
36+
log_pattern_network_connected,
37+
timeout=timeout,
38+
start_pos=dut_cloud.uart.get_size(),
39+
)
3240

3341
logger.info("Device connected to LTE network.")
3442

43+
3544
def _disconnect_network_and_clear_modem_credentials(dut_cloud, sec_tag: int):
3645
logger.info("Disconnecting network and clearing modem credentials...")
3746

3847
log_pattern_network_disconnected = "network: Network connectivity lost"
3948
dut_cloud.uart.write("att_network disconnect\r\n")
40-
dut_cloud.uart.wait_for_str(log_pattern_network_disconnected, timeout=20)
49+
dut_cloud.uart.wait_for_str(
50+
log_pattern_network_disconnected,
51+
timeout=20,
52+
start_pos=dut_cloud.uart.get_size(),
53+
)
4154

4255
# Clear any existing credentials for the given security tag
4356
dut_cloud.uart.write(f"at AT%CMNG=3,{sec_tag},0\r\n")
57+
time.sleep(1)
4458
dut_cloud.uart.write(f"at AT%CMNG=3,{sec_tag},1\r\n")
59+
time.sleep(1)
4560
dut_cloud.uart.write(f"at AT%CMNG=3,{sec_tag},2\r\n")
61+
time.sleep(1)
4662

4763
logger.info("Modem credentials cleared.")
4864

65+
4966
def _get_attestation_token_from_device(dut_cloud) -> str:
5067
logger.info("Getting attestation token from device...")
5168

5269
dut_cloud.uart.at_cmd_write("at AT%ATTESTTOKEN\r\n")
53-
token_match = dut_cloud.uart.wait_for_str_re(r'%ATTESTTOKEN: "([^"]+)"', timeout=20)
70+
token_match = dut_cloud.uart.wait_for_str_re(
71+
r'%ATTESTTOKEN: "([^"]+)"', timeout=20
72+
)
5473

5574
assert token_match, "No attestation token found"
5675
attestation_token = token_match[0]
5776
logger.info(f"Attestation Token: {attestation_token}")
5877

5978
return attestation_token
6079

80+
6181
def _unclaim_device_from_nrf_cloud_if_exists(dut_cloud):
6282
"""
6383
Attempts to unclaim the device from nRF Cloud.
@@ -70,42 +90,75 @@ def _unclaim_device_from_nrf_cloud_if_exists(dut_cloud):
7090
logger.info(f"Unclaim device status_code: {status_code}")
7191
except requests.exceptions.HTTPError as e:
7292
if e.response.status_code == 404:
73-
logger.info(f"Device {dut_cloud.device_id} not found or already unclaimed (404), proceeding.")
93+
logger.info(
94+
f"Device {dut_cloud.device_id} not found or already unclaimed (404), proceeding."
95+
)
7496
else:
7597
logger.error(f"Error unclaiming device: {e}")
76-
raise # Re-raise other HTTP errors
98+
raise # Re-raise other HTTP errors
99+
77100

78101
def _connect_to_network_and_wait_for_claiming_prompt(dut_cloud):
79102
logger.info("Connecting to network and waiting for device to request claiming...")
80103

81104
log_pattern_network_connected = "network: Network connectivity established"
82-
log_pattern_need_claiming = "Claim the device using the device's attestation token on nrfcloud.com"
105+
log_pattern_need_claiming = (
106+
"Claim the device using the device's attestation token on nrfcloud.com"
107+
)
83108

84109
dut_cloud.uart.write("att_network connect\r\n")
85-
dut_cloud.uart.wait_for_str(log_pattern_network_connected, timeout=240)
86-
dut_cloud.uart.wait_for_str(log_pattern_need_claiming, timeout=240)
110+
dut_cloud.uart.wait_for_str(
111+
log_pattern_network_connected, timeout=240, start_pos=dut_cloud.uart.get_size()
112+
)
113+
dut_cloud.uart.wait_for_str(
114+
log_pattern_need_claiming, timeout=240, start_pos=dut_cloud.uart.get_size()
115+
)
87116

88117
logger.info("Device is ready to be claimed.")
89118

119+
90120
def _claim_device_on_nrf_cloud(dut_cloud, attestation_token: str):
91121
logger.info("Claiming device on nRF Cloud...")
92122

93123
dut_cloud.cloud.claim_device(attestation_token=attestation_token)
94124

95125
logger.info("Device claimed successfully.")
96126

97-
def _wait_for_provisioning_completion_and_cloud_connection(dut_cloud, timeout: int = 240):
98-
logger.info("Waiting for provisioning to complete and device to connect to nRF Cloud...")
99127

100-
dut_cloud.uart.wait_for_str("cloud: Provisioning finished", timeout=timeout)
101-
dut_cloud.uart.wait_for_str("cloud: Connected to Cloud", timeout=timeout)
128+
def _wait_for_provisioning_completion_and_cloud_connection(
129+
dut_cloud, timeout: int = 240
130+
):
131+
logger.info(
132+
"Waiting for provisioning to complete and device to connect to nRF Cloud..."
133+
)
134+
135+
dut_cloud.uart.wait_for_str(
136+
[
137+
"cloud: nrf_provisioning_callback: Provisioning finished",
138+
"cloud: Connected to Cloud",
139+
],
140+
timeout=timeout,
141+
start_pos=dut_cloud.uart.get_size(),
142+
)
102143

103144
logger.info("Device provisioned and connected to nRF Cloud.")
104145

105-
def _verify_device_location_data(dut_cloud, expected_lat: float = 61.5, expected_lon: float = 10.5, lat_tolerance: float = 2.0, lon_tolerance: float = 1.0, timeout: int = 300):
146+
147+
def _verify_device_location_data(
148+
dut_cloud,
149+
expected_lat: float = 61.5,
150+
expected_lon: float = 10.5,
151+
lat_tolerance: float = 2.0,
152+
lon_tolerance: float = 1.0,
153+
timeout: int = 300,
154+
):
106155
logger.info("Verifying device location data...")
107156

108-
values = dut_cloud.uart.wait_for_str_re(r'location_event_handler: Got location: lat: ([\d.-]+), lon: ([\d.-]+), acc: ([\d.-]+), method:', timeout=timeout)
157+
values = dut_cloud.uart.wait_for_str_re(
158+
r"location_event_handler: Got location: lat: ([\d.-]+), lon: ([\d.-]+), acc: ([\d.-]+),method:",
159+
timeout=timeout,
160+
start_pos=dut_cloud.uart.get_size(),
161+
)
109162
assert values, "Failed to get location data from device."
110163

111164
lat_str, lon_str, acc_str = values
@@ -117,6 +170,7 @@ def _verify_device_location_data(dut_cloud, expected_lat: float = 61.5, expected
117170

118171
logger.info("Device location data verified.")
119172

173+
120174
def _trigger_device_reprovisioning_with_new_credentials(dut_cloud, sec_tag: int):
121175
"""
122176
Initiates the reprovisioning process on the device by:
@@ -127,26 +181,25 @@ def _trigger_device_reprovisioning_with_new_credentials(dut_cloud, sec_tag: int)
127181
logger.info("Starting reprovisioning process with new credentials...")
128182

129183
# Prepare the reprovisioning command for nRF Cloud
130-
command_json = json.dumps({
131-
"description": "Reprovisioning with new cloud credentials",
132-
"request": {
133-
"cloudAccessKeyGeneration": {
134-
"secTag": sec_tag
135-
}
184+
command_json = json.dumps(
185+
{
186+
"description": "Reprovisioning with new cloud credentials",
187+
"request": {"cloudAccessKeyGeneration": {"secTag": sec_tag}},
136188
}
137-
})
189+
)
138190

139191
logger.info(f"Adding reprovisioning command to nRF Cloud: {command_json}")
140192

141193
dut_cloud.cloud.add_provisioning_command(
142-
device_id=dut_cloud.device_id,
143-
command=command_json
194+
device_id=dut_cloud.device_id, command=command_json
144195
)
145196

146197
# Update device shadow to indicate a new provisioning command is available
147198
logger.info("Updating device shadow to trigger reprovisioning command processing.")
148199

149-
dut_cloud.cloud.patch_delete_command_entry_from_shadow(device_id=dut_cloud.device_id)
200+
dut_cloud.cloud.patch_delete_command_entry_from_shadow(
201+
device_id=dut_cloud.device_id
202+
)
150203
dut_cloud.cloud.patch_add_provisioning_command_to_shadow(
151204
device_id=dut_cloud.device_id,
152205
command=1, # Command ID 1 signifies a provisioning request
@@ -157,6 +210,7 @@ def _trigger_device_reprovisioning_with_new_credentials(dut_cloud, sec_tag: int)
157210

158211
dut_cloud.uart.write("att_button_press 1\r\n")
159212

213+
160214
def _trigger_device_reprovisioning_expecting_no_commands(dut_cloud):
161215
"""
162216
Initiates a reprovisioning check on the device when no new commands are expected from nRF Cloud.
@@ -167,7 +221,9 @@ def _trigger_device_reprovisioning_expecting_no_commands(dut_cloud):
167221
# Update device shadow to indicate a provisioning check (even if no commands are queued)
168222
logger.info("Updating device shadow to trigger provisioning check.")
169223

170-
dut_cloud.cloud.patch_delete_command_entry_from_shadow(device_id=dut_cloud.device_id)
224+
dut_cloud.cloud.patch_delete_command_entry_from_shadow(
225+
device_id=dut_cloud.device_id
226+
)
171227
dut_cloud.cloud.patch_add_provisioning_command_to_shadow(
172228
device_id=dut_cloud.device_id,
173229
command=1, # Command ID 1 typically signifies a provisioning request
@@ -178,8 +234,10 @@ def _trigger_device_reprovisioning_expecting_no_commands(dut_cloud):
178234

179235
dut_cloud.uart.write("att_button_press 1\r\n")
180236

237+
181238
# --- Test Phases ---
182239

240+
183241
def _run_initial_provisioning(dut_cloud, hex_file):
184242
logger.info("--- Starting Phase 1: Initial Device Provisioning ---")
185243

@@ -195,6 +253,7 @@ def _run_initial_provisioning(dut_cloud, hex_file):
195253

196254
logger.info("--- Phase 1: Initial Device Provisioning Completed Successfully ---")
197255

256+
198257
def _run_reprovisioning(dut_cloud):
199258
logger.info("--- Starting Phase 2: Reprovisioning with New Credentials ---")
200259

@@ -205,7 +264,10 @@ def _run_reprovisioning(dut_cloud):
205264
_wait_for_provisioning_completion_and_cloud_connection(dut_cloud, timeout=300)
206265
_verify_device_location_data(dut_cloud)
207266

208-
logger.info("--- Phase 2: Reprovisioning with New Credentials Completed Successfully ---")
267+
logger.info(
268+
"--- Phase 2: Reprovisioning with New Credentials Completed Successfully ---"
269+
)
270+
209271

210272
def _run_reprovisioning_expecting_no_commands(dut_cloud):
211273
logger.info("--- Starting Phase 3: Reprovisioning Expecting No Commands ---")
@@ -214,14 +276,22 @@ def _run_reprovisioning_expecting_no_commands(dut_cloud):
214276

215277
_trigger_device_reprovisioning_expecting_no_commands(dut_cloud)
216278
# Verify the device correctly handles the absence of new provisioning commands
217-
dut_cloud.uart.wait_for_str("cloud: No commands from the nRF Provisioning Service to process", timeout=300)
218-
dut_cloud.uart.wait_for_str("cloud: Connected to Cloud", timeout=240) # Ensure it reconnects
279+
dut_cloud.uart.wait_for_str(
280+
[
281+
"cloud: No commands from the nRF Provisioning Service to process",
282+
"cloud: Connected to Cloud",
283+
],
284+
timeout=300,
285+
start_pos=dut_cloud.uart.get_size(),
286+
)
287+
logger.info(
288+
"--- Phase 3: Reprovisioning Expecting No Commands Completed Successfully ---"
289+
)
219290

220-
logger.info("--- Phase 3: Reprovisioning Expecting No Commands Completed Successfully ---")
221291

222292
# --- Main Test ---
223-
224-
def test_device_provisioning(dut_cloud, hex_file):
293+
@pytest.mark.parametrize("_", range(1, 3))
294+
def test_device_provisioning(_, dut_cloud, hex_file):
225295
"""
226296
Tests the full device provisioning and reprovisioning lifecycle:
227297
1. Initial provisioning: Flashes, gets attestation token, claims on nRF Cloud, connects.
@@ -231,12 +301,6 @@ def test_device_provisioning(dut_cloud, hex_file):
231301
connects to nRF Cloud Provisioning Service, but finds no new commands to process and establishes
232302
a connection without reprovisioning.
233303
"""
234-
235-
for i in range(1, 21):
236-
logger.info(f"--- Starting Test Iteration: {i}/20 ---")
237-
_run_initial_provisioning(dut_cloud, hex_file)
238-
_run_reprovisioning(dut_cloud)
239-
_run_reprovisioning_expecting_no_commands(dut_cloud)
240-
logger.info(f"--- Completed Test Iteration: {i}/20 ---")
241-
242-
logger.info("Device provisioning and reprovisioning test completed successfully.")
304+
_run_initial_provisioning(dut_cloud, hex_file)
305+
_run_reprovisioning(dut_cloud)
306+
_run_reprovisioning_expecting_no_commands(dut_cloud)

tests/on_target/utils/logger.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
def get_logger(log_level = LOG_LEVEL):
2323
caller = inspect.stack()[1]
24-
filename = caller.filename
24+
filename = caller.filename.split(".")[0]
2525

2626
# Global Logging Function
2727
logger = logging.getLogger(filename)
@@ -34,7 +34,7 @@ def get_logger(log_level = LOG_LEVEL):
3434
console.setLevel(log_level)
3535
logger.addHandler(console)
3636

37-
formatter = '%(asctime)s - %(filename)s - %(levelname)s - %(message)s'
37+
formatter = '%(asctime)s:%(filename)s:%(levelname)s:%(message)s'
3838

3939
formatter = ColoredFormatter(formatter, datefmt='%H:%M:%S')
4040
console.setFormatter(formatter)
@@ -45,7 +45,7 @@ def get_logger(log_level = LOG_LEVEL):
4545
file_handler = logging.FileHandler(f"{LOG_DIR}/{LOG_FILENAME}_{level}.txt")
4646
file_handler.setLevel(level.upper())
4747
logger.addHandler(file_handler)
48-
formatter = '%(asctime)s - %(filename)s - %(levelname)s - %(message)s'
48+
formatter = '%(asctime)s:%(filename)s:%(levelname)s:%(message)s'
4949
file_handler.setFormatter(logging.Formatter(formatter, datefmt='%Y-%m-%d %H:%M:%S'))
5050

5151
logger.setLevel(logging.DEBUG)

0 commit comments

Comments
 (0)