blob: 9ee37f2edcefbeb2adf3d5f767356f886cb5dbe7 [file] [log] [blame]
Philipp Wagner03aaf322020-09-09 19:11:44 +01001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4
5import locale
6import logging
7import os
8import re
9import shlex
10import signal
11import subprocess
12import time
13from pathlib import Path
14
15import serial
16
17log = logging.getLogger(__name__)
18
19
20class Process:
21 def __init__(self,
22 cmd,
23 logdir,
24 cwd=None,
25 startup_done_expect=None,
26 startup_timeout=None,
27 default_filter_func=None):
28 """Utility class used to spawn an interact with processes.
29
30 Args:
31 cmd: Command line argument used to spawn process.
32 logdir: Directory used to store STDOUT and STDERR output.
33 cwd: Working directory used to spawn process.
34 startup_done_expect: Pattern used to check at process startup time.
35 startup_timeout: Timeout in seconds for |startup_done_expect| check.
36 default_filter_func: Default function to filter all stdout/stderr
37 output with when matching it with find_in_output().
38 """
39 if isinstance(cmd, str):
40 self.cmd = shlex.split(cmd)
41 else:
42 self.cmd = [str(c) for c in cmd]
43 self.logdir = str(logdir)
44 self.cwd = str(cwd)
45 self.startup_done_expect = startup_done_expect
46 self.startup_timeout = startup_timeout
47 self.proc = None
48 self.logger = logging.getLogger(__name__)
49
50 self._f_stdout = None
51 self._f_stderr = None
52 self._f_stdout_r = None
53 self._f_stderr_r = None
54
55 self.default_filter_func = default_filter_func
56
57 def __del__(self):
58 try:
59 self.proc.kill()
60 self._f_stdout.close()
61 self._f_stderr.close()
62 self._f_stdout_r.close()
63 self._f_stderr_r.close()
64 except Exception:
65 pass
66
67 def is_running(self):
68 """ Check if the process is running
69
70 Returns:
71 True if the process is running, False otherwise
72 """
73 return self.proc and self.proc.poll() is not None
74
75 def run(self):
76 """Start process with command line configured in constructor."""
77 cmd_name = os.path.basename(self.cmd[0])
78
79 # Enforce line-buffered STDOUT even when sending STDOUT/STDERR to file.
80 # If applications don't fflush() STDOUT manually, STDOUT goes through
81 # a 4kB buffer before we see any output, which prevents searching for
82 # the string indicating a successful startup.
83 # see discussion at http://www.pixelbeat.org/programming/stdio_buffering/
84 cmd = ['stdbuf', '-oL'] + self.cmd
Philipp Wagner4fffa382021-02-23 17:57:55 +000085 self.logger.info("Running command " +
86 ' '.join(shlex.quote(w) for w in cmd))
Philipp Wagner03aaf322020-09-09 19:11:44 +010087
88 logfile_stdout = os.path.join(self.logdir,
89 "{}.stdout".format(cmd_name))
90 logfile_stderr = os.path.join(self.logdir,
91 "{}.stderr".format(cmd_name))
92 self.logger.debug("Capturing STDOUT at " + logfile_stdout)
93 self.logger.debug("Capturing STDERR at " + logfile_stderr)
94
95 self._f_stdout = open(logfile_stdout, 'w')
96 self._f_stderr = open(logfile_stderr, 'w')
97 self.proc = subprocess.Popen(cmd,
98 cwd=self.cwd,
99 universal_newlines=True,
Rupert Swarbrickd8dca9f2020-10-27 13:43:01 +0000100 bufsize=1, # Use line buffering
Philipp Wagner03aaf322020-09-09 19:11:44 +0100101 stdin=subprocess.PIPE,
102 stdout=self._f_stdout,
103 stderr=self._f_stderr)
104
105 self._f_stdout_r = open(logfile_stdout, 'r')
106 self._f_stderr_r = open(logfile_stderr, 'r')
107
108 # no startup match pattern given => startup done!
109 if self.startup_done_expect is None:
110 return True
111
112 # check if the string indicating a successful startup appears in the
Rupert Swarbrick1de9b332020-10-27 13:39:42 +0000113 # the program output (STDOUT or STDERR).
114 try:
115 init_done = self.find_in_output(pattern=self.startup_done_expect,
116 timeout=self.startup_timeout)
117 if init_done is None:
118 raise RuntimeError('No match for pattern {!r} in startup.'
119 .format(str(self.startup_done_expect)))
120 except subprocess.TimeoutExpired as err:
121 # On time-out, find_in_output will raise a TimeoutExpired exception
122 # but (of course) it doesn't know the command it's running, so we
123 # amend it as it goes past.
124 assert err.cmd is None
125 err.cmd = cmd
126 raise
Philipp Wagner03aaf322020-09-09 19:11:44 +0100127
128 self.logger.info("Startup sequence matched, startup done.")
129
130 return True
131
132 def terminate(self):
133 """Terminates process started by run call."""
134 if self.proc is not None:
135 self.proc.terminate()
136
137 def send_ctrl_c(self):
138 """Sends SIGINT to process started by run call."""
139 if self.proc is not None:
140 self.proc.send_signal(signal.SIGINT)
141
142 def expect(self, stdin_data=None, pattern=None, timeout=None):
143 """Write data to STDIN and check if the output is as expected.
144
145 Args:
146 stdin_data: Data to send to STDIN.
147 pattern: Pattern to search for after sending |stdin_data|.
148 timeout: Timeout in seconds for |pattern| check.
149 Returns:
150 True if |pattern| found, False otherwise.
151 """
152 # We don't get STDOUT/STDERR from subprocess.communicate() as it's
153 # redirected to file. We need to read the files instead.
154
155 # XXX: races (false positives) can happen here if output is generated
156 # before the input is sent to the process.
157 if pattern is None:
158 self._f_stdout_r.seek(0, 2)
159
160 self.proc.stdin.write(stdin_data)
161 self.proc.stdin.flush()
162
163 if pattern is None:
164 return True
165
166 return self.find_in_output(pattern, timeout) is not None
167
168 def find_in_output(self,
169 pattern,
170 timeout,
171 from_start=False,
172 filter_func=None):
173 """Read STDOUT and STDERR to find an expected pattern.
174
175 See find_in_files() for more documentation.
176 """
177
178 if filter_func is None:
179 filter_func = self.default_filter_func
180
181 def wait_func():
182 """ Wait up to 0.2s until the process terminates.
183
184 Returns:
185 True if the subprocess terminated and a further wait will not
186 produce more output, False otherwise.
187 """
188 try:
189 self.proc.wait(timeout=0.2)
190 except subprocess.TimeoutExpired:
191 # The process did not yet terminate.
192 return False
193
194 # The process did terminate.
195 return True
196
197 return find_in_files([self._f_stdout_r, self._f_stderr_r],
198 pattern,
199 timeout,
200 filter_func=filter_func,
201 from_start=from_start,
202 wait_func=wait_func)
203
204
205class LoggingSerial(serial.Serial):
Pirmin Vogelad2afea2021-05-20 10:43:40 +0200206 """ Access to a serial console which logs all read/written data to file. """
Philipp Wagner03aaf322020-09-09 19:11:44 +0100207 def __init__(self,
208 *args,
209 log_dir_path,
210 default_filter_func=None,
211 **kwargs):
212 super().__init__(*args, **kwargs)
213
214 self._log_to_device_fp = open(str(log_dir_path / 'to-device.log'),
215 'wb')
216 self._log_from_device_fp = open(str(log_dir_path / 'from-device.log'),
217 'wb')
218
219 self.default_filter_func = default_filter_func
220
221 log.debug("Logging UART communication for {} to {}".format(
222 self.port, str(log_dir_path)))
223
224 def read(self, size=1):
225 bytes = super().read(size)
226 self._log_from_device_fp.write(bytes)
227
228 # Explicitly flush the log file to ensure that data is present in the
229 # log file when the conftest log dumping routines read the file after a
230 # failed test.
231 self._log_from_device_fp.flush()
232
233 return bytes
234
235 def write(self, data):
236 retcode = super().write(data)
237 self._log_to_device_fp.write(data)
238
239 # Explicitly flush the log file to ensure that data is present in the
240 # log file when the conftest log dumping routines read the file after a
241 # failed test.
242 self._log_to_device_fp.flush()
243
244 return retcode
245
246 def close(self):
247 if self._log_to_device_fp:
248 self._log_to_device_fp.close()
249
250 if self._log_from_device_fp:
251 self._log_from_device_fp.close()
252
253 super().close()
254
Philipp Wagner9cc67662020-09-09 19:47:41 +0100255 def log_add_marker(self, text: str):
256 """ Write a marker text into the UART send and receive log files """
257 text_b = text.encode('UTF-8')
258 self._log_to_device_fp.write(text_b)
259 self._log_from_device_fp.write(text_b)
260
261 def drain_in(self, timeout=None, silence_time=.5):
262 """ Read all available input data
263
264 Args:
265 timeout: Maximum time this function blocks, in seconds.
266 silence_time: Consider the input drained if no new data can be read
267 after this time, in seconds.
268 Returns:
269 The data read from the device.
270 """
271 t_end = None
272 if timeout is not None:
273 t_end = time.time() + timeout
274
275 read_data = b''
276
277 first_iteration = True
278 while first_iteration or self.in_waiting != 0:
279 if timeout is not None and time.time() >= t_end:
280 break
281
282 read_data += self.read(self.in_waiting)
283 first_iteration = False
284 time.sleep(silence_time)
285
286 return read_data
287
Philipp Wagner03aaf322020-09-09 19:11:44 +0100288 def find_in_output(self, pattern, timeout=None, filter_func=None):
289 """ Expect a pattern to appear in one of the output lines.
290
291 See the documentation of match_line() for a description of |pattern|.
292
293 Args:
294 pattern: Pattern to search for
295 timeout: Timeout in seconds for |pattern| check.
296 filter_func: Function to filter the output with before applying
297 |pattern|. If none, the |self.default_filter_func| is used.
298 Returns:
299 None if |pattern| was not found, the return value of match_line()
300 otherwise.
301 """
302
303 if filter_func is None:
304 filter_func = self.default_filter_func
305
306 t_end = None
307 if timeout is not None:
308 t_end = time.time() + timeout
309
310 line = self.readline()
311 while True:
312 m = match_line(line, pattern, filter_func)
313 if m is not None:
314 return m
315
316 if timeout is not None and time.time() >= t_end:
317 break
318
319 line = self.readline()
320
321 return None
322
323
324def dump_temp_files(tmp_path):
325 """ Dump all files in a directory (typically logs) """
326 logging.debug(
327 "================= DUMP OF ALL TEMPORARY FILES =================")
328
329 tmp_files = [
330 Path(root) / f for root, dirs, files in os.walk(str(tmp_path))
331 for f in files
332 ]
333
334 textchars = bytearray({7, 8, 9, 10, 12, 13, 27} |
335 set(range(0x20, 0x100)) - {0x7f})
336
337 def is_binary_string(bytes):
338 return bool(bytes.translate(None, textchars))
339
340 for f in tmp_files:
341 if f.name == '.lock':
342 continue
343
344 logging.debug("vvvvvvvvvvvvvvvvvvvv {} vvvvvvvvvvvvvvvvvvvv".format(f))
345
346 if not f.is_file():
347 logging.debug("[Not a regular file.]")
348 elif f.stat().st_size > 50 * 1024:
349 logging.debug("[File is too large to be shown ({} bytes).]".format(
350 f.stat().st_size))
351 else:
352 with open(str(f), 'rb') as fp:
353 data = fp.read(1024)
354 if is_binary_string(data):
355 logging.debug(
356 "[File contains {} bytes of binary data.]".format(
357 f.stat().st_size))
358 else:
359 fp.seek(0)
360 for line in fp:
361 line_str = line.decode(locale.getpreferredencoding(),
362 errors='backslashreplace')
363 logging.debug(line_str.rstrip())
364 logging.debug(
365 "^^^^^^^^^^^^^^^^^^^^ {} ^^^^^^^^^^^^^^^^^^^^\n".format(f))
366
367
Philipp Wagnerbb63cc92021-02-01 12:09:31 +0000368def load_sw_over_spi(tmp_path,
Pirmin Vogelad2afea2021-05-20 10:43:40 +0200369 loader_path,
Philipp Wagnerbb63cc92021-02-01 12:09:31 +0000370 sw_test_bin,
Pirmin Vogelad2afea2021-05-20 10:43:40 +0200371 loader_args=[],
Philipp Wagnerbb63cc92021-02-01 12:09:31 +0000372 timeout=600):
Pirmin Vogelad2afea2021-05-20 10:43:40 +0200373 """ Use the specified loader utility to load software onto a device. """
Philipp Wagner03aaf322020-09-09 19:11:44 +0100374
375 log.info("Flashing device software from {} over SPI".format(
376 str(sw_test_bin)))
377
Pirmin Vogelad2afea2021-05-20 10:43:40 +0200378 # cw310_loader.py supports both loading bistream and firmware.
379 if re.search('cw310_loader', str(loader_path)):
380 bin_arg = '--firmware'
381 else: # spiflash loads firmware only.
382 bin_arg = '--input'
383 cmd_flash = [loader_path, bin_arg, sw_test_bin] + loader_args
Philipp Wagner03aaf322020-09-09 19:11:44 +0100384 p_flash = Process(cmd_flash, logdir=tmp_path, cwd=tmp_path)
385 p_flash.run()
Philipp Wagnerbb63cc92021-02-01 12:09:31 +0000386 p_flash.proc.wait(timeout=timeout)
Philipp Wagner03aaf322020-09-09 19:11:44 +0100387 assert p_flash.proc.returncode == 0
388
389 log.info("Device software flashed.")
390
391
392def find_in_files(file_objects,
393 pattern,
394 timeout,
395 from_start=False,
396 filter_func=None,
397 wait_func=None):
398 """Find a pattern in a list of file objects (file descriptors)
399
400 See the documentation of match_line() for a description of |pattern|.
401
402 Args:
403 pattern: Pattern to search for
404 timeout: Timeout in seconds for |pattern| check. Set to None to wait
405 indefinitely.
406 from_start: Search from the start of the given file objects.
407 filter_func: Function to filter the output with before applying
408 |pattern|. If none, the |default_filter_func| is used.
409 wait_func: Function to call to wait. The function is expected to return
410 True if no further output is expected in |file_objects| (this will
411 end the wait loop before |timeout| expires), and False otherwise
412 (more output could be produced).
Rupert Swarbrick1de9b332020-10-27 13:39:42 +0000413
Philipp Wagner03aaf322020-09-09 19:11:44 +0100414 Returns:
Rupert Swarbrick1de9b332020-10-27 13:39:42 +0000415 If |pattern| was found, returns the return value of match_line().
416 Otherwise, returns None if |wait_func| returned True (signalling the
417 end of the input with no match).
418
419 Raises a subprocess.TimeoutExpired exception on timeout. This will have
420 a |cmd| field of None.
421
Philipp Wagner03aaf322020-09-09 19:11:44 +0100422 """
423
424 t_end = None
425 if timeout is not None:
426 t_end = time.time() + timeout
427
428 if wait_func is None:
429 # By default, sleep for 200 ms when waiting for new input.
430 def wait_func():
431 time.sleep(.2)
432 return False
433
434 if from_start:
435 for file_object in file_objects:
436 file_object.seek(0)
437
Philipp Wagnerad0fb5d2021-01-19 17:08:03 +0000438 end_loop = False
Philipp Wagner03aaf322020-09-09 19:11:44 +0100439 while True:
Philipp Wagner03aaf322020-09-09 19:11:44 +0100440 for file_object in file_objects:
Philipp Wagner03aaf322020-09-09 19:11:44 +0100441 for line in file_object:
Philipp Wagner03aaf322020-09-09 19:11:44 +0100442 m = match_line(line.rstrip(), pattern, filter_func)
443 if m is not None:
444 return m
445
Philipp Wagnerad0fb5d2021-01-19 17:08:03 +0000446 if end_loop:
447 break
448
Philipp Wagner52fce152020-11-06 15:38:15 +0000449 if timeout is not None and time.time() >= t_end:
Rupert Swarbrick1de9b332020-10-27 13:39:42 +0000450 raise subprocess.TimeoutExpired(None, timeout)
Philipp Wagner03aaf322020-09-09 19:11:44 +0100451
Philipp Wagnerad0fb5d2021-01-19 17:08:03 +0000452 # The wait function returns True to indicate that no more data will be
453 # produced (e.g. because the producing process terminated). But we still
454 # need to check one last time if the already produced data is matching
455 # the `pattern`.
Philipp Wagner03aaf322020-09-09 19:11:44 +0100456 end_loop = wait_func()
Philipp Wagner03aaf322020-09-09 19:11:44 +0100457
458 return None
459
460
461def match_line(line, pattern, filter_func=None):
462 """
463 Check if a line matches a pattern
464
465 Line endings (CR/LR) are removed from |line| and neither matched nor
466 returned.
467
468 pattern can be of two types:
469 1. A regular expression (any object with a match attribute, or a re.Pattern
470 object in Python 3.7+). In this case, the pattern is matched against all
471 lines and the result of re.match(pattern) (a re.Match object since
Philipp Wagner2ba5f992021-02-24 14:46:17 +0000472 Python 3.7) is returned. Note that re.match() always matches from the
473 beginning of the line.
Philipp Wagner03aaf322020-09-09 19:11:44 +0100474 2. A string. In this case pattern is compared to each line with
475 startswith(), and the full matching line is returned on a match.
476 If no match is found None is returned.
477
478 Optionally apply a filter to the line before matching it.
479 """
480
481 if isinstance(line, bytes):
482 line = line.rstrip(b'\r\n')
483 else:
484 line = line.rstrip('\r\n')
485
486 if filter_func is not None:
487 line = filter_func(line)
488
489 # TODO: Check for an instance of re.Pattern once we move to Python 3.7+
490 if hasattr(pattern, "match"):
491 return pattern.match(line)
492 else:
493 if line.startswith(pattern):
494 return line
495
496 return None
497
498
499def filter_remove_device_sw_log_prefix(line):
500 """
501 Remove the file/line information in log messages produced by device software
502 LOG() macros.
503 """
504
505 # See base_log_internal_core() in lib/base/log.c for the format description.
506 pattern = r'^[IWEF?]\d{5} [a-zA-Z0-9\.-_]+:\d+\] '
507 if isinstance(line, bytes):
508 return re.sub(bytes(pattern, encoding='utf-8'), b'', line)
509 else:
510 return re.sub(pattern, '', line)
Philipp Wagnerc208de02021-01-19 17:20:11 +0000511
512
513def filter_remove_sw_test_status_log_prefix(line):
514 """
515 Remove the logging prefix produced by the sw_test_status DV component.
516 """
517
518 # Example of a full prefix to be matched:
Pirmin Vogel25105dd2021-05-24 14:28:23 +0200519 # 1629002: (../src/lowrisc_dv_sw_test_status_0/sw_test_status_if.sv:42) [TOP.chip_earlgrey_verilator.u_sw_test_status_if] # noqa: E501
Philipp Wagnerc208de02021-01-19 17:20:11 +0000520 pattern = r'\d+: \(.+/sw_test_status_if\.sv:\d+\) \[TOP\..+\] '
521 if isinstance(line, bytes):
522 return re.sub(bytes(pattern, encoding='utf-8'), b'', line)
523 else:
524 return re.sub(pattern, '', line)