run_test_idf_monitor.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env python
  2. #
  3. # SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
  4. # SPDX-License-Identifier: Apache-2.0
  5. from __future__ import print_function, unicode_literals
  6. import errno
  7. import filecmp
  8. import os
  9. import pty
  10. import re
  11. import socket
  12. import subprocess
  13. import sys
  14. import tempfile
  15. import threading
  16. import time
  17. from builtins import object
  18. from io import open
  19. XTENSA_ARGS = '--toolchain-prefix xtensa-esp32-elf-'
  20. RISCV_ARGS = '--decode-panic backtrace --target esp32c3 --toolchain-prefix riscv32-esp-elf-'
  21. test_list = (
  22. # Add new tests here. All files should be placed in IN_DIR. Columns are
  23. # Input file Filter string File with expected output Timeout ELF file Extra args
  24. ('in1.txt', '', 'in1f1.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  25. ('in1.txt', '*:V', 'in1f1.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  26. ('in1.txt', 'hello_world', 'in1f2.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  27. ('in1.txt', '*:N', 'in1f3.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  28. ('in2.txt', 'boot mdf_device_handle:I mesh:E vfs:I', 'in2f1.txt', 420, 'dummy_xtensa.elf', XTENSA_ARGS),
  29. ('in2.txt', 'vfs', 'in2f2.txt', 420, 'dummy_xtensa.elf', XTENSA_ARGS),
  30. ('core1.txt', '', 'core1_out.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  31. ('riscv_panic1.txt', '', 'riscv_panic1_out.txt', 60, 'dummy_riscv.elf', RISCV_ARGS),
  32. )
  33. IN_DIR = 'tests/' # tests are in this directory
  34. OUT_DIR = 'outputs/' # test results are written to this directory (kept only for debugging purposes)
  35. ERR_OUT = 'monitor_error_output.'
  36. IDF_MONITOR_WAPPER = 'idf_monitor_wrapper.py'
  37. SERIAL_ALIVE_FILE = '/tmp/serial_alive' # the existence of this file signalize that idf_monitor is ready to receive
  38. # connection related to communicating with idf_monitor through sockets
  39. HOST = 'localhost'
  40. # blocking socket operations are used with timeout:
  41. SOCKET_TIMEOUT = 30
  42. # the test is restarted after failure (idf_monitor has to be killed):
  43. RETRIES_PER_TEST = 2
  44. COREDUMP_VERSION_REGEX = r'espcoredump\.py v\d+\.[\d\w-]+(\.[\d\w-]+)?'
  45. def remove_coredump_version_string(file_path):
  46. with open(file_path, 'r') as file:
  47. init_text = file.read()
  48. modified_text = re.sub(COREDUMP_VERSION_REGEX, '', init_text, re.MULTILINE)
  49. if not init_text != modified_text:
  50. return None
  51. with tempfile.NamedTemporaryFile(delete=False) as temp_file:
  52. temp_file.write(modified_text.encode())
  53. return temp_file.name
  54. def monitor_timeout(process):
  55. if process.poll() is None:
  56. # idf_monitor_wrapper is still running
  57. try:
  58. process.kill()
  59. print('\tidf_monitor_wrapper was killed because it did not finish in time.')
  60. except OSError as e:
  61. if e.errno == errno.ESRCH:
  62. # ignores a possible race condition which can occur when the process exits between poll() and kill()
  63. pass
  64. else:
  65. raise
  66. class TestRunner(object):
  67. def __enter__(self):
  68. self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  69. self.serversocket.bind((HOST, 0))
  70. self.port = self.serversocket.getsockname()[1]
  71. self.serversocket.listen(5)
  72. return self
  73. def __exit__(self, type, value, traceback):
  74. try:
  75. self.serversocket.shutdown(socket.SHUT_RDWR)
  76. self.serversocket.close()
  77. print('Socket was closed successfully')
  78. except (OSError, socket.error):
  79. pass
  80. def accept_connection(self):
  81. """ returns a socket for sending the input for idf_monitor which must be closed before calling this again. """
  82. (clientsocket, address) = self.serversocket.accept()
  83. # exception will be thrown here if the idf_monitor didn't connect in time
  84. return clientsocket
  85. def test_iteration(runner, test):
  86. try:
  87. # Make sure that the file doesn't exist. It will be recreated by idf_monitor_wrapper.py
  88. os.remove(SERIAL_ALIVE_FILE)
  89. except OSError:
  90. pass
  91. print('\nRunning test on {} with filter "{}" and expecting {}'.format(test[0], test[1], test[2]))
  92. try:
  93. with open(OUT_DIR + test[2], 'w', encoding='utf-8') as o_f, \
  94. tempfile.NamedTemporaryFile(dir=OUT_DIR, prefix=ERR_OUT, mode='w', delete=False) as e_f:
  95. monitor_cmd = [sys.executable, IDF_MONITOR_WAPPER,
  96. '--port', 'socket://{}:{}?logging=debug'.format(HOST, runner.port),
  97. '--print_filter', test[1],
  98. '--serial_alive_file', SERIAL_ALIVE_FILE,
  99. '--elf-file', test[4]]
  100. monitor_cmd += test[5].split()
  101. (master_fd, slave_fd) = pty.openpty()
  102. print('\t', ' '.join(monitor_cmd), sep='')
  103. print('\tstdout="{}" stderr="{}" stdin="{}"'.format(o_f.name, e_f.name, os.ttyname(slave_fd)))
  104. print('\tMonitor timeout: {} seconds'.format(test[3]))
  105. start = time.time()
  106. # the server socket is alive so idf_monitor can start now
  107. proc = subprocess.Popen(monitor_cmd, stdin=slave_fd, stdout=o_f, stderr=e_f, close_fds=True, bufsize=0)
  108. # - idf_monitor's stdin needs to be connected to some pseudo-tty in docker image even when it is not
  109. # used at all
  110. # - setting bufsize is needed because the default value is different on Python 2 and 3
  111. # - the default close_fds is also different on Python 2 and 3
  112. monitor_watchdog = threading.Timer(test[3], monitor_timeout, [proc])
  113. monitor_watchdog.start()
  114. client = runner.accept_connection()
  115. # The connection is ready but idf_monitor cannot yet receive data (the serial reader thread is not running).
  116. # This seems to happen on Ubuntu 16.04 LTS and is not related to the version of Python or pyserial.
  117. # Updating to Ubuntu 18.04 LTS also helps but here, a workaround is used: A wrapper is used for IDF monitor
  118. # which checks the serial reader thread and creates a file when it is running.
  119. while not os.path.isfile(SERIAL_ALIVE_FILE) and proc.poll() is None:
  120. print('\tSerial reader is not ready. Do a sleep...')
  121. time.sleep(1)
  122. # Only now can we send the inputs:
  123. with open(IN_DIR + test[0], 'rb') as f:
  124. print('\tSending {} to the socket'.format(f.name))
  125. for chunk in iter(lambda: f.read(1024), b''):
  126. client.sendall(chunk)
  127. idf_exit_sequence = b'\x1d\n'
  128. print('\tSending <exit> to the socket')
  129. client.sendall(idf_exit_sequence)
  130. close_end_time = start + 0.75 * test[3] # time when the process is close to be killed
  131. while True:
  132. ret = proc.poll()
  133. if ret is not None:
  134. break
  135. if time.time() > close_end_time:
  136. # The process isn't finished yet so we are starting to send additional exit sequences because maybe
  137. # the other end didn't received it.
  138. print('\tSending additional <exit> to the socket')
  139. client.sendall(idf_exit_sequence)
  140. time.sleep(1)
  141. end = time.time()
  142. print('\tidf_monitor exited after {:.2f} seconds'.format(end - start))
  143. if ret < 0:
  144. raise RuntimeError('idf_monitor was terminated by signal {}'.format(-ret))
  145. # idf_monitor needs to end before the socket is closed in order to exit without an exception.
  146. finally:
  147. if monitor_watchdog:
  148. monitor_watchdog.cancel()
  149. os.close(slave_fd)
  150. os.close(master_fd)
  151. if client:
  152. client.close()
  153. print('\tThe client was closed successfully')
  154. f1 = IN_DIR + test[2]
  155. f2 = OUT_DIR + test[2]
  156. temp_f1, temp_f2 = remove_coredump_version_string(f1), remove_coredump_version_string(f2)
  157. print('\tdiff {} {}'.format(f1, f2))
  158. if temp_f1 and temp_f2:
  159. f1, f2 = temp_f1, temp_f2
  160. if filecmp.cmp(f1, f2, shallow=False):
  161. print('\tTest has passed')
  162. else:
  163. raise RuntimeError('The contents of the files are different. Please examine the artifacts.')
  164. def main():
  165. gstart = time.time()
  166. if not os.path.exists(OUT_DIR):
  167. os.mkdir(OUT_DIR)
  168. socket.setdefaulttimeout(SOCKET_TIMEOUT)
  169. for test in test_list:
  170. for i in range(RETRIES_PER_TEST):
  171. with TestRunner() as runner:
  172. # Each test (and each retry) is run with a different port (and server socket). This is done for
  173. # the CI run where retry with a different socket is necessary to pass the test. According to the
  174. # experiments, retry with the same port (and server socket) is not sufficient.
  175. try:
  176. test_iteration(runner, test)
  177. # no more retries if test_iteration exited without an exception
  178. break
  179. except Exception as e:
  180. if i < RETRIES_PER_TEST - 1:
  181. print('Test has failed with exception:', e)
  182. print('Another attempt will be made.')
  183. else:
  184. raise
  185. gend = time.time()
  186. print('Execution took {:.2f} seconds\n'.format(gend - gstart))
  187. if __name__ == '__main__':
  188. main()