diff --git a/examples/protocols/https_server/wss_server/wss_server_example_test.py b/examples/protocols/https_server/wss_server/wss_server_example_test.py new file mode 100644 index 0000000000..d829ecb80c --- /dev/null +++ b/examples/protocols/https_server/wss_server/wss_server_example_test.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +# +# Copyright 2021 Espressif Systems (Shanghai) CO LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import division, print_function, unicode_literals + +import os +import re +import threading +import time +from types import TracebackType +from typing import Any, Optional + +import tiny_test_fw +import ttfw_idf +import websocket +from tiny_test_fw import Utility + +OPCODE_TEXT = 0x1 +OPCODE_BIN = 0x2 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa +CORRECT_ASYNC_DATA = 'Hello client' + + +class WsClient: + def __init__(self, ip, port, ca_file): # type: (str, int, str) -> None + self.port = port + self.ip = ip + sslopt = {'ca_certs':ca_file, 'check_hostname': False} + self.ws = websocket.WebSocket(sslopt=sslopt) + # Set timeout to 10 seconds to avoid conection failure at the time of handshake + self.ws.settimeout(10) + + def __enter__(self): # type: ignore + self.ws.connect('wss://{}:{}/ws'.format(self.ip, self.port)) + return self + + def __exit__(self, exc_type, exc_value, traceback): # type: (type, RuntimeError, TracebackType) -> None + self.ws.close() + + def read(self): # type: () -> Any + return self.ws.recv_data(control_frame=True) + + def write(self, data, opcode=OPCODE_TEXT): # type: (str, int) -> Any + if opcode == OPCODE_PING: + return self.ws.ping(data) + if opcode == OPCODE_PONG: + return self.ws.pong(data) + return self.ws.send(data) + + +class wss_client_thread(threading.Thread): + def __init__(self, ip, port, ca_file): # type: (str, int, str) -> None + threading.Thread.__init__(self) + self.ip = ip + self.port = port + self.ca_file = ca_file + self.start_time = time.time() + self.exc = None + self.data = 'Espressif' + self.async_response = False + + def run(self): # type: () -> None + with WsClient(self.ip, self.port, self.ca_file) as ws: + while True: + try: + opcode, data = ws.read() + data = data.decode('UTF-8') + + if opcode == OPCODE_PING: + ws.write(data=self.data, opcode=OPCODE_PONG) + if opcode == OPCODE_TEXT: + if data == CORRECT_ASYNC_DATA: + self.async_response = True + Utility.console_log('Thread {} obtained correct async message'.format(self.name)) + # Keep sending pong to update the keepalive in the server + if (time.time() - self.start_time) > 20: + break + except Exception as e: + Utility.console_log('Failed to connect to the client and read async data') + self.exc = e # type: ignore + if self.async_response is not True: + self.exc = RuntimeError('Failed to obtain correct async data') # type: ignore + + def join(self, timeout=0): # type:(Optional[float]) -> None + threading.Thread.join(self) + if self.exc: + raise self.exc + + +def test_multiple_client_keep_alive_and_async_response(ip, port, ca_file): # type: (str, int, str) -> None + threads = [] + for _ in range(3): + try: + thread = wss_client_thread(ip, port, ca_file) + thread.start() + threads.append(thread) + except OSError: + Utility.console_log('Error: unable to start thread') + # keep delay of 5 seconds between two connections to avoid handshake timeout + time.sleep(5) + + for t in threads: + t.join() + + +@ttfw_idf.idf_example_test(env_tag='Example_WIFI') +def test_examples_protocol_https_wss_server(env, extra_data): # type: (tiny_test_fw.Env.Env, None) -> None # pylint: disable=unused-argument + + # Acquire DUT + dut1 = env.get_dut('https_server', 'examples/protocols/https_server/wss_server', dut_class=ttfw_idf.ESP32DUT) + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, 'wss_server.bin') + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance('https_wss_server_bin_size', '{}KB'.format(bin_size // 1024)) + + # Upload binary and start testing + Utility.console_log('Starting wss_server test app') + dut1.start_app() + + # Parse IP address of STA + got_port = dut1.expect(re.compile(r'Server listening on port (\d+)'), timeout=60)[0] + Utility.console_log('Waiting to connect with AP') + got_ip = dut1.expect(re.compile(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)'), timeout=60)[0] + + Utility.console_log('Got IP : ' + got_ip) + Utility.console_log('Got Port : ' + got_port) + + ca_file = os.path.join(os.path.dirname(__file__), 'main', 'certs', 'cacert.pem') + # Start ws server test + with WsClient(got_ip, int(got_port), ca_file) as ws: + # Check for echo + DATA = 'Espressif' + dut1.expect('performing session handshake') + client_fd = dut1.expect(re.compile(r'New client connected (\d+)'), timeout=20)[0] + ws.write(data=DATA, opcode=OPCODE_TEXT) + dut1.expect(re.compile(r'Received packet with message: {}'.format(DATA))) + opcode, data = ws.read() + data = data.decode('UTF-8') + if data != DATA: + raise RuntimeError('Failed to receive the correct echo response') + Utility.console_log('Correct echo response obtained from the wss server') + + # Test for keepalive + Utility.console_log('Testing for keep alive (approx time = 20s)') + start_time = time.time() + while True: + try: + opcode, data = ws.read() + if opcode == OPCODE_PING: + ws.write(data='Espressif', opcode=OPCODE_PONG) + Utility.console_log('Received PING, replying PONG (to update the keepalive)') + # Keep sending pong to update the keepalive in the server + if (time.time() - start_time) > 20: + break + except Exception: + Utility.console_log('Failed the test for keep alive,\nthe client got abruptly disconnected') + raise + + # keepalive timeout is 10 seconds so do not respond for (10 + 1) senconds + Utility.console_log('Testing if client is disconnected if it does not respond for 10s i.e. keep_alive timeout (approx time = 11s)') + try: + dut1.expect('Client not alive, closing fd {}'.format(client_fd), timeout=20) + dut1.expect('Client disconnected {}'.format(client_fd)) + except Exception: + Utility.console_log('ENV_ERROR:Failed the test for keep alive,\nthe connection was not closed after timeout') + + time.sleep(11) + Utility.console_log('Passed the test for keep alive') + + # Test keep alive and async response for multiple simultaneous client connections + Utility.console_log('Testing for multiple simultaneous client connections (approx time = 30s)') + test_multiple_client_keep_alive_and_async_response(got_ip, int(got_port), ca_file) + Utility.console_log('Passed the test for multiple simultaneous client connections') + + +if __name__ == '__main__': + test_examples_protocol_https_wss_server() # pylint: disable=no-value-for-parameter