DUT.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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. import time
  35. import re
  36. import threading
  37. import copy
  38. import sys
  39. import functools
  40. import serial
  41. from serial.tools import list_ports
  42. import Utility
  43. if sys.version_info[0] == 2:
  44. import Queue as _queue
  45. else:
  46. import queue as _queue
  47. class ExpectTimeout(ValueError):
  48. """ timeout for expect method """
  49. pass
  50. class UnsupportedExpectItem(ValueError):
  51. """ expect item not supported by the expect method """
  52. pass
  53. def _expect_lock(func):
  54. @functools.wraps(func)
  55. def handler(self, *args, **kwargs):
  56. with self.expect_lock:
  57. ret = func(self, *args, **kwargs)
  58. return ret
  59. return handler
  60. def _decode_data(data):
  61. """ for python3, if the data is bytes, then decode it to string """
  62. if isinstance(data, bytes):
  63. # convert bytes to string
  64. try:
  65. data = data.decode("utf-8", "ignore")
  66. except UnicodeDecodeError:
  67. data = data.decode("iso8859-1", )
  68. return data
  69. class _DataCache(_queue.Queue):
  70. """
  71. Data cache based on Queue. Allow users to process data cache based on bytes instead of Queue."
  72. """
  73. def __init__(self, maxsize=0):
  74. _queue.Queue.__init__(self, maxsize=maxsize)
  75. self.data_cache = str()
  76. def get_data(self, timeout=0):
  77. """
  78. get a copy of data from cache.
  79. :param timeout: timeout for waiting new queue item
  80. :return: copy of data cache
  81. """
  82. # make sure timeout is non-negative
  83. if timeout < 0:
  84. timeout = 0
  85. try:
  86. data = self.get(timeout=timeout)
  87. self.data_cache += _decode_data(data)
  88. except _queue.Empty:
  89. # don't do anything when on update for cache
  90. pass
  91. return copy.deepcopy(self.data_cache)
  92. def flush(self, index=0xFFFFFFFF):
  93. """
  94. flush data from cache.
  95. :param index: if < 0 then don't do flush, otherwise flush data before index
  96. :return: None
  97. """
  98. # first add data in queue to cache
  99. self.get_data()
  100. if index > 0:
  101. self.data_cache = self.data_cache[index:]
  102. class _RecvThread(threading.Thread):
  103. PERFORMANCE_PATTERN = re.compile(r"\[Performance]\[(\w+)]: ([^\r\n]+)\r?\n")
  104. def __init__(self, read, data_cache):
  105. super(_RecvThread, self).__init__()
  106. self.exit_event = threading.Event()
  107. self.setDaemon(True)
  108. self.read = read
  109. self.data_cache = data_cache
  110. # cache the last line of recv data for collecting performance
  111. self._line_cache = str()
  112. def collect_performance(self, data):
  113. """ collect performance """
  114. if data:
  115. decoded_data = _decode_data(data)
  116. matches = self.PERFORMANCE_PATTERN.findall(self._line_cache + decoded_data)
  117. for match in matches:
  118. Utility.console_log("[Performance][{}]: {}".format(match[0], match[1]),
  119. color="orange")
  120. # cache incomplete line to later process
  121. lines = decoded_data.splitlines(True)
  122. last_line = lines[-1]
  123. if last_line[-1] != "\n":
  124. if len(lines) == 1:
  125. # only one line and the line is not finished, then append this to cache
  126. self._line_cache += lines[-1]
  127. else:
  128. # more than one line and not finished, replace line cache
  129. self._line_cache = lines[-1]
  130. else:
  131. # line finishes, flush cache
  132. self._line_cache = str()
  133. def run(self):
  134. while not self.exit_event.isSet():
  135. data = self.read(1000)
  136. if data:
  137. self.data_cache.put(data)
  138. self.collect_performance(data)
  139. def exit(self):
  140. self.exit_event.set()
  141. self.join()
  142. class BaseDUT(object):
  143. """
  144. :param name: application defined name for port
  145. :param port: comport name, used to create DUT port
  146. :param log_file: log file name
  147. :param app: test app instance
  148. :param kwargs: extra args for DUT to create ports
  149. """
  150. DEFAULT_EXPECT_TIMEOUT = 5
  151. def __init__(self, name, port, log_file, app, **kwargs):
  152. self.expect_lock = threading.Lock()
  153. self.name = name
  154. self.port = port
  155. self.log_file = log_file
  156. self.app = app
  157. self.data_cache = _DataCache()
  158. self.receive_thread = None
  159. # open and start during init
  160. self.open()
  161. def __str__(self):
  162. return "DUT({}: {})".format(self.name, str(self.port))
  163. # define for methods need to be overwritten by Port
  164. @classmethod
  165. def list_available_ports(cls):
  166. """
  167. list all available ports.
  168. subclass (port) must overwrite this method.
  169. :return: list of available comports
  170. """
  171. pass
  172. def _port_open(self):
  173. """
  174. open the port.
  175. subclass (port) must overwrite this method.
  176. :return: None
  177. """
  178. pass
  179. def _port_read(self, size=1):
  180. """
  181. read form port. This method should not blocking for long time, otherwise receive thread can not exit.
  182. subclass (port) must overwrite this method.
  183. :param size: max size to read.
  184. :return: read data.
  185. """
  186. pass
  187. def _port_write(self, data):
  188. """
  189. write to port.
  190. subclass (port) must overwrite this method.
  191. :param data: data to write
  192. :return: None
  193. """
  194. pass
  195. def _port_close(self):
  196. """
  197. close port.
  198. subclass (port) must overwrite this method.
  199. :return: None
  200. """
  201. pass
  202. # methods that need to be overwritten by Tool
  203. @classmethod
  204. def confirm_dut(cls, port, app, **kwargs):
  205. """
  206. confirm if it's a DUT, usually used by auto detecting DUT in by Env config.
  207. subclass (tool) must overwrite this method.
  208. :param port: comport
  209. :param app: app instance
  210. :return: True or False
  211. """
  212. pass
  213. def start_app(self):
  214. """
  215. usually after we got DUT, we need to do some extra works to let App start.
  216. For example, we need to reset->download->reset to let IDF application start on DUT.
  217. subclass (tool) must overwrite this method.
  218. :return: None
  219. """
  220. pass
  221. # methods that features raw port methods
  222. def open(self):
  223. """
  224. open port and create thread to receive data.
  225. :return: None
  226. """
  227. self._port_open()
  228. self.receive_thread = _RecvThread(self._port_read, self.data_cache)
  229. self.receive_thread.start()
  230. def close(self):
  231. """
  232. close receive thread and then close port.
  233. :return: None
  234. """
  235. if self.receive_thread:
  236. self.receive_thread.exit()
  237. self._port_close()
  238. def write(self, data, eol="\r\n", flush=True):
  239. """
  240. :param data: data
  241. :param eol: end of line pattern.
  242. :param flush: if need to flush received data cache before write data.
  243. usually we need to flush data before write,
  244. make sure processing outputs generated by wrote.
  245. :return: None
  246. """
  247. # do flush before write
  248. if flush:
  249. self.data_cache.flush()
  250. # do write if cache
  251. if data is not None:
  252. self._port_write(data + eol if eol else data)
  253. @_expect_lock
  254. def read(self, size=0xFFFFFFFF):
  255. """
  256. read(size=0xFFFFFFFF)
  257. read raw data. NOT suggested to use this method.
  258. Only use it if expect method doesn't meet your requirement.
  259. :param size: read size. default read all data
  260. :return: read data
  261. """
  262. data = self.data_cache.get_data(0)[:size]
  263. self.data_cache.flush(size)
  264. return data
  265. # expect related methods
  266. @staticmethod
  267. def _expect_str(data, pattern):
  268. """
  269. protected method. check if string is matched in data cache.
  270. :param data: data to process
  271. :param pattern: string
  272. :return: pattern if match succeed otherwise None
  273. """
  274. index = data.find(pattern)
  275. if index != -1:
  276. ret = pattern
  277. index += len(pattern)
  278. else:
  279. ret = None
  280. return ret, index
  281. @staticmethod
  282. def _expect_re(data, pattern):
  283. """
  284. protected method. check if re pattern is matched in data cache
  285. :param data: data to process
  286. :param pattern: compiled RegEx pattern
  287. :return: match groups if match succeed otherwise None
  288. """
  289. ret = None
  290. match = pattern.search(data)
  291. if match:
  292. ret = match.groups()
  293. index = match.end()
  294. else:
  295. index = -1
  296. return ret, index
  297. EXPECT_METHOD = [
  298. [type(re.compile("")), "_expect_re"],
  299. [str, "_expect_str"],
  300. ]
  301. def _get_expect_method(self, pattern):
  302. """
  303. protected method. get expect method according to pattern type.
  304. :param pattern: expect pattern, string or compiled RegEx
  305. :return: ``_expect_str`` or ``_expect_re``
  306. """
  307. for expect_method in self.EXPECT_METHOD:
  308. if isinstance(pattern, expect_method[0]):
  309. method = expect_method[1]
  310. break
  311. else:
  312. raise UnsupportedExpectItem()
  313. return self.__getattribute__(method)
  314. @_expect_lock
  315. def expect(self, pattern, timeout=DEFAULT_EXPECT_TIMEOUT):
  316. """
  317. expect(pattern, timeout=DEFAULT_EXPECT_TIMEOUT)
  318. expect received data on DUT match the pattern. will raise exception when expect timeout.
  319. :raise ExpectTimeout: failed to find the pattern before timeout
  320. :raise UnsupportedExpectItem: pattern is not string or compiled RegEx
  321. :param pattern: string or compiled RegEx(string pattern)
  322. :param timeout: timeout for expect
  323. :return: string if pattern is string; matched groups if pattern is RegEx
  324. """
  325. method = self._get_expect_method(pattern)
  326. # non-blocking get data for first time
  327. data = self.data_cache.get_data(0)
  328. start_time = time.time()
  329. while True:
  330. ret, index = method(data, pattern)
  331. if ret is not None or time.time() - start_time > timeout:
  332. self.data_cache.flush(index)
  333. break
  334. # wait for new data from cache
  335. data = self.data_cache.get_data(time.time() + timeout - start_time)
  336. if ret is None:
  337. raise ExpectTimeout(self.name + ": " + str(pattern))
  338. return ret
  339. def _expect_multi(self, expect_all, expect_item_list, timeout):
  340. """
  341. protected method. internal logical for expect multi.
  342. :param expect_all: True or False, expect all items in the list or any in the list
  343. :param expect_item_list: expect item list
  344. :param timeout: timeout
  345. :return: None
  346. """
  347. def process_expected_item(item_raw):
  348. # convert item raw data to standard dict
  349. item = {
  350. "pattern": item_raw[0] if isinstance(item_raw, tuple) else item_raw,
  351. "method": self._get_expect_method(item_raw[0] if isinstance(item_raw, tuple)
  352. else item_raw),
  353. "callback": item_raw[1] if isinstance(item_raw, tuple) else None,
  354. "index": -1,
  355. "ret": None,
  356. }
  357. return item
  358. expect_items = [process_expected_item(x) for x in expect_item_list]
  359. # non-blocking get data for first time
  360. data = self.data_cache.get_data(0)
  361. start_time = time.time()
  362. matched_expect_items = list()
  363. while True:
  364. for expect_item in expect_items:
  365. if expect_item not in matched_expect_items:
  366. # exclude those already matched
  367. expect_item["ret"], expect_item["index"] = \
  368. expect_item["method"](data, expect_item["pattern"])
  369. if expect_item["ret"] is not None:
  370. # match succeed for one item
  371. matched_expect_items.append(expect_item)
  372. break
  373. # if expect all, then all items need to be matched,
  374. # else only one item need to matched
  375. if expect_all:
  376. match_succeed = (matched_expect_items == expect_items)
  377. else:
  378. match_succeed = True if matched_expect_items else False
  379. if time.time() - start_time > timeout or match_succeed:
  380. break
  381. else:
  382. data = self.data_cache.get_data(time.time() + timeout - start_time)
  383. if match_succeed:
  384. # do callback and flush matched data cache
  385. slice_index = -1
  386. for expect_item in matched_expect_items:
  387. # trigger callback
  388. if expect_item["callback"]:
  389. expect_item["callback"](expect_item["ret"])
  390. slice_index = max(slice_index, expect_item["index"])
  391. # flush already matched data
  392. self.data_cache.flush(slice_index)
  393. else:
  394. raise ExpectTimeout(self.name + ": " + str(expect_items))
  395. @_expect_lock
  396. def expect_any(self, *expect_items, **timeout):
  397. """
  398. expect_any(*expect_items, timeout=DEFAULT_TIMEOUT)
  399. expect any of the patterns.
  400. will call callback (if provided) if pattern match succeed and then return.
  401. will pass match result to the callback.
  402. :raise ExpectTimeout: failed to match any one of the expect items before timeout
  403. :raise UnsupportedExpectItem: pattern in expect_item is not string or compiled RegEx
  404. :arg expect_items: one or more expect items.
  405. string, compiled RegEx pattern or (string or RegEx(string pattern), callback)
  406. :keyword timeout: timeout for expect
  407. :return: None
  408. """
  409. # to be compatible with python2
  410. # in python3 we can write f(self, *expect_items, timeout=DEFAULT_TIMEOUT)
  411. if "timeout" not in timeout:
  412. timeout["timeout"] = self.DEFAULT_EXPECT_TIMEOUT
  413. return self._expect_multi(False, expect_items, **timeout)
  414. @_expect_lock
  415. def expect_all(self, *expect_items, **timeout):
  416. """
  417. expect_all(*expect_items, timeout=DEFAULT_TIMEOUT)
  418. expect all of the patterns.
  419. will call callback (if provided) if all pattern match succeed and then return.
  420. will pass match result to the callback.
  421. :raise ExpectTimeout: failed to match all of the expect items before timeout
  422. :raise UnsupportedExpectItem: pattern in expect_item is not string or compiled RegEx
  423. :arg expect_items: one or more expect items.
  424. string, compiled RegEx pattern or (string or RegEx(string pattern), callback)
  425. :keyword timeout: timeout for expect
  426. :return: None
  427. """
  428. # to be compatible with python2
  429. # in python3 we can write f(self, *expect_items, timeout=DEFAULT_TIMEOUT)
  430. if "timeout" not in timeout:
  431. timeout["timeout"] = self.DEFAULT_EXPECT_TIMEOUT
  432. return self._expect_multi(True, expect_items, **timeout)
  433. class SerialDUT(BaseDUT):
  434. """ serial with logging received data feature """
  435. DEFAULT_UART_CONFIG = {
  436. "baudrate": 115200,
  437. "bytesize": serial.EIGHTBITS,
  438. "parity": serial.PARITY_NONE,
  439. "stopbits": serial.STOPBITS_ONE,
  440. "timeout": 0.05,
  441. "xonxoff": False,
  442. "rtscts": False,
  443. }
  444. def __init__(self, name, port, log_file, app, **kwargs):
  445. self.port_inst = None
  446. self.serial_configs = self.DEFAULT_UART_CONFIG.copy()
  447. self.serial_configs.update(kwargs)
  448. super(SerialDUT, self).__init__(name, port, log_file, app, **kwargs)
  449. @staticmethod
  450. def _format_data(data):
  451. """
  452. format data for logging. do decode and add timestamp.
  453. :param data: raw data from read
  454. :return: formatted data (str)
  455. """
  456. timestamp = time.time()
  457. timestamp = "[{}:{}]".format(time.strftime("%m-%d %H:%M:%S", time.localtime(timestamp)),
  458. str(timestamp % 1)[2:5])
  459. formatted_data = timestamp.encode() + b"\r\n" + data + b"\r\n"
  460. return formatted_data
  461. def _port_open(self):
  462. self.port_inst = serial.Serial(self.port, **self.serial_configs)
  463. def _port_close(self):
  464. self.port_inst.close()
  465. def _port_read(self, size=1):
  466. data = self.port_inst.read(size)
  467. if data:
  468. with open(self.log_file, "ab+") as _log_file:
  469. _log_file.write(self._format_data(data))
  470. return data
  471. def _port_write(self, data):
  472. if isinstance(data, str):
  473. data = data.encode()
  474. self.port_inst.write(data)
  475. @classmethod
  476. def list_available_ports(cls):
  477. return [x.device for x in list_ports.comports()]