DUT.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. # SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. """
  4. DUT provides 3 major groups of features:
  5. * DUT port feature, provide basic open/close/read/write features
  6. * DUT tools, provide extra methods to control the device, like download and start app
  7. * DUT expect method, provide features for users to check DUT outputs
  8. The current design of DUT have 3 classes for one DUT: BaseDUT, DUTPort, DUTTool.
  9. * BaseDUT class:
  10. * defines methods DUT port and DUT tool need to overwrite
  11. * provide the expect methods and some other methods based on DUTPort
  12. * DUTPort class:
  13. * inherent from BaseDUT class
  14. * implements the port features by overwriting port methods defined in BaseDUT
  15. * DUTTool class:
  16. * inherent from one of the DUTPort class
  17. * implements the tools features by overwriting tool methods defined in BaseDUT
  18. * could add some new methods provided by the tool
  19. This module implements the BaseDUT class and one of the port class SerialDUT.
  20. User should implement their DUTTool classes.
  21. If they using different port then need to implement their DUTPort class as well.
  22. """
  23. from __future__ import print_function
  24. import copy
  25. import functools
  26. import re
  27. import sys
  28. import threading
  29. import time
  30. # python2 and python3 queue package name is different
  31. try:
  32. import Queue as _queue
  33. except ImportError:
  34. import queue as _queue # type: ignore
  35. try:
  36. from typing import Callable, List
  37. except ImportError:
  38. # Only used for type annotations
  39. pass
  40. import serial
  41. from serial.tools import list_ports
  42. from . import Utility
  43. class ExpectTimeout(ValueError):
  44. """ timeout for expect method """
  45. pass
  46. class UnsupportedExpectItem(ValueError):
  47. """ expect item not supported by the expect method """
  48. pass
  49. def _expect_lock(func):
  50. @functools.wraps(func)
  51. def handler(self, *args, **kwargs):
  52. with self.expect_lock:
  53. ret = func(self, *args, **kwargs)
  54. return ret
  55. return handler
  56. def _decode_data(data):
  57. """ for python3, if the data is bytes, then decode it to string """
  58. if isinstance(data, bytes):
  59. # convert bytes to string. This is a bit of a hack, we know that we want to log this
  60. # later so encode to the stdout encoding with backslash escapes for anything non-encodable
  61. try:
  62. return data.decode(sys.stdout.encoding, 'backslashreplace')
  63. except UnicodeDecodeError: # Python <3.5 doesn't support backslashreplace
  64. return data.decode(sys.stdout.encoding, 'replace')
  65. return data
  66. def _pattern_to_string(pattern):
  67. try:
  68. ret = 'RegEx: ' + pattern.pattern
  69. except AttributeError:
  70. ret = pattern
  71. return ret
  72. class _DataCache(_queue.Queue):
  73. """
  74. Data cache based on Queue. Allow users to process data cache based on bytes instead of Queue."
  75. """
  76. def __init__(self, maxsize=0):
  77. _queue.Queue.__init__(self, maxsize=maxsize)
  78. self.data_cache = str()
  79. def _move_from_queue_to_cache(self):
  80. """
  81. move all of the available data in the queue to cache
  82. :return: True if moved any item from queue to data cache, else False
  83. """
  84. ret = False
  85. while True:
  86. try:
  87. self.data_cache += _decode_data(self.get(0))
  88. ret = True
  89. except _queue.Empty:
  90. break
  91. return ret
  92. def get_data(self, timeout=0.0):
  93. """
  94. get a copy of data from cache.
  95. :param timeout: timeout for waiting new queue item
  96. :return: copy of data cache
  97. """
  98. # make sure timeout is non-negative
  99. if timeout < 0:
  100. timeout = 0
  101. ret = self._move_from_queue_to_cache()
  102. if not ret:
  103. # we only wait for new data if we can't provide a new data_cache
  104. try:
  105. data = self.get(timeout=timeout)
  106. self.data_cache += _decode_data(data)
  107. except _queue.Empty:
  108. # don't do anything when on update for cache
  109. pass
  110. return copy.deepcopy(self.data_cache)
  111. def flush(self, index=0xFFFFFFFF):
  112. """
  113. flush data from cache.
  114. :param index: if < 0 then don't do flush, otherwise flush data before index
  115. :return: None
  116. """
  117. # first add data in queue to cache
  118. self.get_data()
  119. if index > 0:
  120. self.data_cache = self.data_cache[index:]
  121. class _LogThread(threading.Thread, _queue.Queue):
  122. """
  123. We found some SD card on Raspberry Pi could have very bad performance.
  124. It could take seconds to save small amount of data.
  125. If the DUT receives data and save it as log, then it stops receiving data until log is saved.
  126. This could lead to expect timeout.
  127. As an workaround to this issue, ``BaseDUT`` class will create a thread to save logs.
  128. Then data will be passed to ``expect`` as soon as received.
  129. """
  130. def __init__(self):
  131. threading.Thread.__init__(self, name='LogThread')
  132. _queue.Queue.__init__(self, maxsize=0)
  133. self.setDaemon(True)
  134. self.flush_lock = threading.Lock()
  135. def save_log(self, filename, data):
  136. """
  137. :param filename: log file name
  138. :param data: log data. Must be ``bytes``.
  139. """
  140. self.put({'filename': filename, 'data': data})
  141. def flush_data(self):
  142. with self.flush_lock:
  143. data_cache = dict()
  144. while True:
  145. # move all data from queue to data cache
  146. try:
  147. log = self.get_nowait()
  148. try:
  149. data_cache[log['filename']] += log['data']
  150. except KeyError:
  151. data_cache[log['filename']] = log['data']
  152. except _queue.Empty:
  153. break
  154. # flush data
  155. for filename in data_cache:
  156. with open(filename, 'ab+') as f:
  157. f.write(data_cache[filename])
  158. def run(self):
  159. while True:
  160. time.sleep(1)
  161. self.flush_data()
  162. class RecvThread(threading.Thread):
  163. CHECK_FUNCTIONS = [] # type: List[Callable]
  164. """ DUT subclass can define a few check functions to process received data. """
  165. def __init__(self, read, dut):
  166. super(RecvThread, self).__init__()
  167. self.exit_event = threading.Event()
  168. self.setDaemon(True)
  169. self.read = read
  170. self.dut = dut
  171. self.data_cache = dut.data_cache
  172. self.recorded_data = dut.recorded_data
  173. self.record_data_lock = dut.record_data_lock
  174. self._line_cache = str()
  175. def _line_completion(self, data):
  176. """
  177. Usually check functions requires to check for one complete line.
  178. This method will do line completion for the first line, and strip incomplete last line.
  179. """
  180. ret = self._line_cache
  181. decoded_data = _decode_data(data)
  182. # cache incomplete line to later process
  183. lines = decoded_data.splitlines(True)
  184. last_line = lines[-1]
  185. if last_line[-1] != '\n':
  186. if len(lines) == 1:
  187. # only one line and the line is not finished, then append this to cache
  188. self._line_cache += lines[-1]
  189. ret = str()
  190. else:
  191. # more than one line and not finished, replace line cache
  192. self._line_cache = lines[-1]
  193. ret += ''.join(lines[:-1])
  194. else:
  195. # line finishes, flush cache
  196. self._line_cache = str()
  197. ret += decoded_data
  198. return ret
  199. def run(self):
  200. while not self.exit_event.isSet():
  201. raw_data = self.read(1000)
  202. if raw_data:
  203. # we need to do line completion before call check functions
  204. # need to call check functions first
  205. # otherwise check functions could be called after cases finished
  206. comp_data = self._line_completion(raw_data)
  207. for check_function in self.CHECK_FUNCTIONS:
  208. check_function(self, comp_data)
  209. with self.record_data_lock:
  210. self.data_cache.put(raw_data)
  211. for capture_id in self.recorded_data:
  212. self.recorded_data[capture_id].put(raw_data)
  213. def exit(self):
  214. self.exit_event.set()
  215. self.join()
  216. class BaseDUT(object):
  217. """
  218. :param name: application defined name for port
  219. :param port: comport name, used to create DUT port
  220. :param log_file: log file name
  221. :param app: test app instance
  222. :param kwargs: extra args for DUT to create ports
  223. """
  224. DEFAULT_EXPECT_TIMEOUT = 10
  225. MAX_EXPECT_FAILURES_TO_SAVED = 10
  226. RECV_THREAD_CLS = RecvThread
  227. TARGET = None
  228. """ DUT subclass can specify RECV_THREAD_CLS to do add some extra stuff when receive data.
  229. For example, DUT can implement exception detect & analysis logic in receive thread subclass. """
  230. LOG_THREAD = _LogThread()
  231. LOG_THREAD.start()
  232. def __init__(self, name, port, log_file, app, **kwargs):
  233. self.expect_lock = threading.Lock()
  234. self.name = name
  235. self.port = port
  236. self.log_file = log_file
  237. self.app = app
  238. self.data_cache = _DataCache()
  239. # the main process of recorded data are done in receive thread
  240. # but receive thread could be closed in DUT lifetime (tool methods)
  241. # so we keep it in BaseDUT, as their life cycle are same
  242. self.recorded_data = dict()
  243. self.record_data_lock = threading.RLock()
  244. self.receive_thread = None
  245. self.expect_failures = []
  246. self._port_open()
  247. self.start_receive()
  248. def __str__(self):
  249. return 'DUT({}: {})'.format(self.name, str(self.port))
  250. def _save_expect_failure(self, pattern, data, start_time):
  251. """
  252. Save expect failure. If the test fails, then it will print the expect failures.
  253. In some cases, user will handle expect exceptions.
  254. The expect failures could be false alarm, and test case might generate a lot of such failures.
  255. Therefore, we don't print the failure immediately and limit the max size of failure list.
  256. """
  257. self.expect_failures.insert(0, {'pattern': pattern, 'data': data,
  258. 'start': start_time, 'end': time.time()})
  259. self.expect_failures = self.expect_failures[:self.MAX_EXPECT_FAILURES_TO_SAVED]
  260. def _save_dut_log(self, data):
  261. """
  262. Save DUT log into file using another thread.
  263. This is a workaround for some devices takes long time for file system operations.
  264. See descriptions in ``_LogThread`` for details.
  265. """
  266. self.LOG_THREAD.save_log(self.log_file, data)
  267. # define for methods need to be overwritten by Port
  268. @classmethod
  269. def list_available_ports(cls):
  270. """
  271. list all available ports.
  272. subclass (port) must overwrite this method.
  273. :return: list of available comports
  274. """
  275. pass
  276. def _port_open(self):
  277. """
  278. open the port.
  279. subclass (port) must overwrite this method.
  280. :return: None
  281. """
  282. pass
  283. def _port_read(self, size=1):
  284. """
  285. read form port. This method should not blocking for long time, otherwise receive thread can not exit.
  286. subclass (port) must overwrite this method.
  287. :param size: max size to read.
  288. :return: read data.
  289. """
  290. pass
  291. def _port_write(self, data):
  292. """
  293. write to port.
  294. subclass (port) must overwrite this method.
  295. :param data: data to write
  296. :return: None
  297. """
  298. pass
  299. def _port_close(self):
  300. """
  301. close port.
  302. subclass (port) must overwrite this method.
  303. :return: None
  304. """
  305. pass
  306. # methods that need to be overwritten by Tool
  307. @classmethod
  308. def confirm_dut(cls, port, **kwargs):
  309. """
  310. confirm if it's a DUT, usually used by auto detecting DUT in by Env config.
  311. subclass (tool) must overwrite this method.
  312. :param port: comport
  313. :return: tuple of result (bool), and target (str)
  314. """
  315. pass
  316. def start_app(self):
  317. """
  318. usually after we got DUT, we need to do some extra works to let App start.
  319. For example, we need to reset->download->reset to let IDF application start on DUT.
  320. subclass (tool) must overwrite this method.
  321. :return: None
  322. """
  323. pass
  324. # methods that features raw port methods
  325. def start_receive(self):
  326. """
  327. Start thread to receive data.
  328. :return: None
  329. """
  330. self.receive_thread = self.RECV_THREAD_CLS(self._port_read, self)
  331. self.receive_thread.start()
  332. def stop_receive(self):
  333. """
  334. stop the receiving thread for the port
  335. :return: None
  336. """
  337. if self.receive_thread:
  338. self.receive_thread.exit()
  339. self.LOG_THREAD.flush_data()
  340. self.receive_thread = None
  341. def close(self):
  342. """
  343. permanently close the port
  344. """
  345. self.stop_receive()
  346. self._port_close()
  347. @staticmethod
  348. def u_to_bytearray(data):
  349. """
  350. if data is not bytearray then it tries to convert it
  351. :param data: data which needs to be checked and maybe transformed
  352. """
  353. if isinstance(data, type(u'')):
  354. try:
  355. data = data.encode('utf-8')
  356. except UnicodeEncodeError as e:
  357. print(u'Cannot encode {} of type {}'.format(data, type(data)))
  358. raise e
  359. return data
  360. def write(self, data, eol='\r\n', flush=True):
  361. """
  362. :param data: data
  363. :param eol: end of line pattern.
  364. :param flush: if need to flush received data cache before write data.
  365. usually we need to flush data before write,
  366. make sure processing outputs generated by wrote.
  367. :return: None
  368. """
  369. # do flush before write
  370. if flush:
  371. self.data_cache.flush()
  372. # do write if cache
  373. if data is not None:
  374. self._port_write(self.u_to_bytearray(data) + self.u_to_bytearray(eol) if eol else self.u_to_bytearray(data))
  375. @_expect_lock
  376. def read(self, size=0xFFFFFFFF):
  377. """
  378. read(size=0xFFFFFFFF)
  379. read raw data. NOT suggested to use this method.
  380. Only use it if expect method doesn't meet your requirement.
  381. :param size: read size. default read all data
  382. :return: read data
  383. """
  384. data = self.data_cache.get_data(0)[:size]
  385. self.data_cache.flush(size)
  386. return data
  387. def start_capture_raw_data(self, capture_id='default'):
  388. """
  389. Sometime application want to get DUT raw data and use ``expect`` method at the same time.
  390. Capture methods provides a way to get raw data without affecting ``expect`` or ``read`` method.
  391. If you call ``start_capture_raw_data`` with same capture id again, it will restart capture on this ID.
  392. :param capture_id: ID of capture. You can use different IDs to do different captures at the same time.
  393. """
  394. with self.record_data_lock:
  395. try:
  396. # if start capture on existed ID, we do flush data and restart capture
  397. self.recorded_data[capture_id].flush()
  398. except KeyError:
  399. # otherwise, create new data cache
  400. self.recorded_data[capture_id] = _DataCache()
  401. def stop_capture_raw_data(self, capture_id='default'):
  402. """
  403. Stop capture and get raw data.
  404. This method should be used after ``start_capture_raw_data`` on the same capture ID.
  405. :param capture_id: ID of capture.
  406. :return: captured raw data between start capture and stop capture.
  407. """
  408. with self.record_data_lock:
  409. try:
  410. ret = self.recorded_data[capture_id].get_data()
  411. self.recorded_data.pop(capture_id)
  412. except KeyError as e:
  413. e.message = 'capture_id does not exist. ' \
  414. 'You should call start_capture_raw_data with same ID ' \
  415. 'before calling stop_capture_raw_data'
  416. raise e
  417. return ret
  418. # expect related methods
  419. @staticmethod
  420. def _expect_str(data, pattern):
  421. """
  422. protected method. check if string is matched in data cache.
  423. :param data: data to process
  424. :param pattern: string
  425. :return: pattern if match succeed otherwise None
  426. """
  427. index = data.find(pattern)
  428. if index != -1:
  429. ret = pattern
  430. index += len(pattern)
  431. else:
  432. ret = None
  433. return ret, index
  434. @staticmethod
  435. def _expect_re(data, pattern):
  436. """
  437. protected method. check if re pattern is matched in data cache
  438. :param data: data to process
  439. :param pattern: compiled RegEx pattern
  440. :return: match groups if match succeed otherwise None
  441. """
  442. ret = None
  443. if isinstance(pattern.pattern, bytes):
  444. pattern = re.compile(_decode_data(pattern.pattern))
  445. match = pattern.search(data)
  446. if match:
  447. ret = tuple(x for x in match.groups())
  448. index = match.end()
  449. else:
  450. index = -1
  451. return ret, index
  452. EXPECT_METHOD = [
  453. [type(re.compile('')), '_expect_re'],
  454. [type(b''), '_expect_str'], # Python 2 & 3 hook to work without 'from builtins import str' from future
  455. [type(u''), '_expect_str'],
  456. ]
  457. def _get_expect_method(self, pattern):
  458. """
  459. protected method. get expect method according to pattern type.
  460. :param pattern: expect pattern, string or compiled RegEx
  461. :return: ``_expect_str`` or ``_expect_re``
  462. """
  463. for expect_method in self.EXPECT_METHOD:
  464. if isinstance(pattern, expect_method[0]):
  465. method = expect_method[1]
  466. break
  467. else:
  468. raise UnsupportedExpectItem()
  469. return self.__getattribute__(method)
  470. @_expect_lock
  471. def expect(self, pattern, timeout=DEFAULT_EXPECT_TIMEOUT, full_stdout=False):
  472. """
  473. expect(pattern, timeout=DEFAULT_EXPECT_TIMEOUT)
  474. expect received data on DUT match the pattern. will raise exception when expect timeout.
  475. :raise ExpectTimeout: failed to find the pattern before timeout
  476. :raise UnsupportedExpectItem: pattern is not string or compiled RegEx
  477. :param pattern: string or compiled RegEx(string pattern)
  478. :param timeout: timeout for expect
  479. :param full_stdout: return full stdout until meet expect string/pattern or just matched string
  480. :return: string if pattern is string; matched groups if pattern is RegEx
  481. """
  482. method = self._get_expect_method(pattern)
  483. stdout = ''
  484. # non-blocking get data for first time
  485. data = self.data_cache.get_data(0)
  486. start_time = time.time()
  487. while True:
  488. ret, index = method(data, pattern)
  489. if ret is not None:
  490. stdout = data[:index]
  491. self.data_cache.flush(index)
  492. break
  493. time_remaining = start_time + timeout - time.time()
  494. if time_remaining < 0:
  495. break
  496. # wait for new data from cache
  497. data = self.data_cache.get_data(time_remaining)
  498. if ret is None:
  499. pattern = _pattern_to_string(pattern)
  500. self._save_expect_failure(pattern, data, start_time)
  501. raise ExpectTimeout(self.name + ': ' + pattern)
  502. return stdout if full_stdout else ret
  503. def _expect_multi(self, expect_all, expect_item_list, timeout):
  504. """
  505. protected method. internal logical for expect multi.
  506. :param expect_all: True or False, expect all items in the list or any in the list
  507. :param expect_item_list: expect item list
  508. :param timeout: timeout
  509. :return: None
  510. """
  511. def process_expected_item(item_raw):
  512. # convert item raw data to standard dict
  513. item = {
  514. 'pattern': item_raw[0] if isinstance(item_raw, tuple) else item_raw,
  515. 'method': self._get_expect_method(item_raw[0] if isinstance(item_raw, tuple)
  516. else item_raw),
  517. 'callback': item_raw[1] if isinstance(item_raw, tuple) else None,
  518. 'index': -1,
  519. 'ret': None,
  520. }
  521. return item
  522. expect_items = [process_expected_item(x) for x in expect_item_list]
  523. # non-blocking get data for first time
  524. data = self.data_cache.get_data(0)
  525. start_time = time.time()
  526. matched_expect_items = list()
  527. while True:
  528. for expect_item in expect_items:
  529. if expect_item not in matched_expect_items:
  530. # exclude those already matched
  531. expect_item['ret'], expect_item['index'] = \
  532. expect_item['method'](data, expect_item['pattern'])
  533. if expect_item['ret'] is not None:
  534. # match succeed for one item
  535. matched_expect_items.append(expect_item)
  536. # if expect all, then all items need to be matched,
  537. # else only one item need to matched
  538. if expect_all:
  539. match_succeed = len(matched_expect_items) == len(expect_items)
  540. else:
  541. match_succeed = True if matched_expect_items else False
  542. time_remaining = start_time + timeout - time.time()
  543. if time_remaining < 0 or match_succeed:
  544. break
  545. else:
  546. data = self.data_cache.get_data(time_remaining)
  547. if match_succeed:
  548. # sort matched items according to order of appearance in the input data,
  549. # so that the callbacks are invoked in correct order
  550. matched_expect_items = sorted(matched_expect_items, key=lambda it: it['index'])
  551. # invoke callbacks and flush matched data cache
  552. slice_index = -1
  553. for expect_item in matched_expect_items:
  554. # trigger callback
  555. if expect_item['callback']:
  556. expect_item['callback'](expect_item['ret'])
  557. slice_index = max(slice_index, expect_item['index'])
  558. # flush already matched data
  559. self.data_cache.flush(slice_index)
  560. else:
  561. pattern = str([_pattern_to_string(x['pattern']) for x in expect_items])
  562. self._save_expect_failure(pattern, data, start_time)
  563. raise ExpectTimeout(self.name + ': ' + pattern)
  564. @_expect_lock
  565. def expect_any(self, *expect_items, **timeout):
  566. """
  567. expect_any(*expect_items, timeout=DEFAULT_TIMEOUT)
  568. expect any of the patterns.
  569. will call callback (if provided) if pattern match succeed and then return.
  570. will pass match result to the callback.
  571. :raise ExpectTimeout: failed to match any one of the expect items before timeout
  572. :raise UnsupportedExpectItem: pattern in expect_item is not string or compiled RegEx
  573. :arg expect_items: one or more expect items.
  574. string, compiled RegEx pattern or (string or RegEx(string pattern), callback)
  575. :keyword timeout: timeout for expect
  576. :return: None
  577. """
  578. # to be compatible with python2
  579. # in python3 we can write f(self, *expect_items, timeout=DEFAULT_TIMEOUT)
  580. if 'timeout' not in timeout:
  581. timeout['timeout'] = self.DEFAULT_EXPECT_TIMEOUT
  582. return self._expect_multi(False, expect_items, **timeout)
  583. @_expect_lock
  584. def expect_all(self, *expect_items, **timeout):
  585. """
  586. expect_all(*expect_items, timeout=DEFAULT_TIMEOUT)
  587. expect all of the patterns.
  588. will call callback (if provided) if all pattern match succeed and then return.
  589. will pass match result to the callback.
  590. :raise ExpectTimeout: failed to match all of the expect items before timeout
  591. :raise UnsupportedExpectItem: pattern in expect_item is not string or compiled RegEx
  592. :arg expect_items: one or more expect items.
  593. string, compiled RegEx pattern or (string or RegEx(string pattern), callback)
  594. :keyword timeout: timeout for expect
  595. :return: None
  596. """
  597. # to be compatible with python2
  598. # in python3 we can write f(self, *expect_items, timeout=DEFAULT_TIMEOUT)
  599. if 'timeout' not in timeout:
  600. timeout['timeout'] = self.DEFAULT_EXPECT_TIMEOUT
  601. return self._expect_multi(True, expect_items, **timeout)
  602. @staticmethod
  603. def _format_ts(ts):
  604. return '{}:{}'.format(time.strftime('%m-%d %H:%M:%S', time.localtime(ts)), str(ts % 1)[2:5])
  605. def print_debug_info(self):
  606. """
  607. Print debug info of current DUT. Currently we will print debug info for expect failures.
  608. """
  609. Utility.console_log('DUT debug info for DUT: {}:'.format(self.name), color='orange')
  610. for failure in self.expect_failures:
  611. Utility.console_log(u'\t[pattern]: {}\r\n\t[data]: {}\r\n\t[time]: {} - {}\r\n'
  612. .format(failure['pattern'], failure['data'],
  613. self._format_ts(failure['start']), self._format_ts(failure['end'])),
  614. color='orange')
  615. class SerialDUT(BaseDUT):
  616. """ serial with logging received data feature """
  617. DEFAULT_UART_CONFIG = {
  618. 'baudrate': 115200,
  619. 'bytesize': serial.EIGHTBITS,
  620. 'parity': serial.PARITY_NONE,
  621. 'stopbits': serial.STOPBITS_ONE,
  622. 'timeout': 0.05,
  623. 'xonxoff': False,
  624. 'rtscts': False,
  625. }
  626. def __init__(self, name, port, log_file, app, **kwargs):
  627. self.port_inst = None
  628. self.serial_configs = self.DEFAULT_UART_CONFIG.copy()
  629. for uart_config_name in self.serial_configs.keys():
  630. if uart_config_name in kwargs:
  631. self.serial_configs[uart_config_name] = kwargs[uart_config_name]
  632. super(SerialDUT, self).__init__(name, port, log_file, app, **kwargs)
  633. def _format_data(self, data):
  634. """
  635. format data for logging. do decode and add timestamp.
  636. :param data: raw data from read
  637. :return: formatted data (str)
  638. """
  639. timestamp = '[{}]'.format(self._format_ts(time.time()))
  640. formatted_data = timestamp.encode() + b'\r\n' + data + b'\r\n'
  641. return formatted_data
  642. def _port_open(self):
  643. self.port_inst = serial.serial_for_url(self.port, **self.serial_configs)
  644. def _port_close(self):
  645. self.port_inst.close()
  646. def _port_read(self, size=1):
  647. data = self.port_inst.read(size)
  648. if data:
  649. self._save_dut_log(self._format_data(data))
  650. return data
  651. def _port_write(self, data):
  652. if isinstance(data, str):
  653. data = data.encode()
  654. self.port_inst.write(data)
  655. @classmethod
  656. def list_available_ports(cls):
  657. return [x.device for x in list_ports.comports()]