DUT.py 24 KB

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