run_test_idf_monitor.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2018-2019 Espressif Systems (Shanghai) PTE LTD
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from __future__ import print_function, unicode_literals
  17. import errno
  18. import filecmp
  19. import os
  20. import pty
  21. import socket
  22. import subprocess
  23. import sys
  24. import tempfile
  25. import threading
  26. import time
  27. from builtins import object
  28. from io import open
  29. XTENSA_ARGS = '--toolchain-prefix xtensa-esp32-elf-'
  30. RISCV_ARGS = '--decode-panic backtrace --target esp32c3 --toolchain-prefix riscv32-esp-elf-'
  31. test_list = (
  32. # Add new tests here. All files should be placed in IN_DIR. Columns are
  33. # Input file Filter string File with expected output Timeout ELF file Extra args
  34. ('in1.txt', '', 'in1f1.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  35. ('in1.txt', '*:V', 'in1f1.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  36. ('in1.txt', 'hello_world', 'in1f2.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  37. ('in1.txt', '*:N', 'in1f3.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  38. ('in2.txt', 'boot mdf_device_handle:I mesh:E vfs:I', 'in2f1.txt', 420, 'dummy_xtensa.elf', XTENSA_ARGS),
  39. ('in2.txt', 'vfs', 'in2f2.txt', 420, 'dummy_xtensa.elf', XTENSA_ARGS),
  40. ('core1.txt', '', 'core1_out.txt', 60, 'dummy_xtensa.elf', XTENSA_ARGS),
  41. ('riscv_panic1.txt', '', 'riscv_panic1_out.txt', 60, 'dummy_riscv.elf', RISCV_ARGS),
  42. )
  43. IN_DIR = 'tests/' # tests are in this directory
  44. OUT_DIR = 'outputs/' # test results are written to this directory (kept only for debugging purposes)
  45. ERR_OUT = 'monitor_error_output.'
  46. IDF_MONITOR_WAPPER = 'idf_monitor_wrapper.py'
  47. SERIAL_ALIVE_FILE = '/tmp/serial_alive' # the existence of this file signalize that idf_monitor is ready to receive
  48. # connection related to communicating with idf_monitor through sockets
  49. HOST = 'localhost'
  50. # blocking socket operations are used with timeout:
  51. SOCKET_TIMEOUT = 30
  52. # the test is restarted after failure (idf_monitor has to be killed):
  53. RETRIES_PER_TEST = 2
  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. print('\tdiff {} {}'.format(f1, f2))
  157. if filecmp.cmp(f1, f2, shallow=False):
  158. print('\tTest has passed')
  159. else:
  160. raise RuntimeError('The contents of the files are different. Please examine the artifacts.')
  161. def main():
  162. gstart = time.time()
  163. if not os.path.exists(OUT_DIR):
  164. os.mkdir(OUT_DIR)
  165. socket.setdefaulttimeout(SOCKET_TIMEOUT)
  166. for test in test_list:
  167. for i in range(RETRIES_PER_TEST):
  168. with TestRunner() as runner:
  169. # Each test (and each retry) is run with a different port (and server socket). This is done for
  170. # the CI run where retry with a different socket is necessary to pass the test. According to the
  171. # experiments, retry with the same port (and server socket) is not sufficient.
  172. try:
  173. test_iteration(runner, test)
  174. # no more retries if test_iteration exited without an exception
  175. break
  176. except Exception as e:
  177. if i < RETRIES_PER_TEST - 1:
  178. print('Test has failed with exception:', e)
  179. print('Another attempt will be made.')
  180. else:
  181. raise
  182. gend = time.time()
  183. print('Execution took {:.2f} seconds\n'.format(gend - gstart))
  184. if __name__ == '__main__':
  185. main()