diff options
Diffstat (limited to 'test/py')
-rw-r--r-- | test/py/conftest.py | 73 | ||||
-rw-r--r-- | test/py/tests/test_spi.py | 20 | ||||
-rw-r--r-- | test/py/u_boot_console_base.py | 123 | ||||
-rw-r--r-- | test/py/u_boot_console_exec_attach.py | 31 | ||||
-rw-r--r-- | test/py/u_boot_spawn.py | 44 |
5 files changed, 228 insertions, 63 deletions
diff --git a/test/py/conftest.py b/test/py/conftest.py index 46a410cf268..d9f074f3817 100644 --- a/test/py/conftest.py +++ b/test/py/conftest.py @@ -23,6 +23,7 @@ from pathlib import Path import pytest import re from _pytest.runner import runtestprotocol +import subprocess import sys from u_boot_spawn import BootFail, Timeout, Unexpected, handle_exception @@ -65,12 +66,16 @@ def pytest_addoption(parser): parser.addoption('--build-dir', default=None, help='U-Boot build directory (O=)') + parser.addoption('--build-dir-extra', default=None, + help='U-Boot build directory for extra build (O=)') parser.addoption('--result-dir', default=None, help='U-Boot test result/tmp directory') parser.addoption('--persistent-data-dir', default=None, help='U-Boot test persistent generated data directory') parser.addoption('--board-type', '--bd', '-B', default='sandbox', help='U-Boot board type') + parser.addoption('--board-type-extra', '--bde', default='sandbox', + help='U-Boot extra board type') parser.addoption('--board-identity', '--id', default='na', help='U-Boot board identity/instance') parser.addoption('--build', default=False, action='store_true', @@ -80,6 +85,9 @@ def pytest_addoption(parser): parser.addoption('--gdbserver', default=None, help='Run sandbox under gdbserver. The argument is the channel '+ 'over which gdbserver should communicate, e.g. localhost:1234') + parser.addoption('--role', help='U-Boot board role (for Labgrid-sjg)') + parser.addoption('--use-running-system', default=False, action='store_true', + help="Assume that U-Boot is ready and don't wait for a prompt") def run_build(config, source_dir, build_dir, board_type, log): """run_build: Build U-Boot @@ -125,26 +133,71 @@ def get_details(config): Returns: tuple: str: Board type (U-Boot build name) + str: Extra board type (where two U-Boot builds are needed) str: Identity for the lab board str: Build directory + str: Extra build directory (where two U-Boot builds are needed) str: Source directory """ - board_type = config.getoption('board_type') - board_identity = config.getoption('board_identity') + role = config.getoption('role') + + # Get a few provided parameters build_dir = config.getoption('build_dir') + build_dir_extra = config.getoption('build_dir_extra') + if role: + # When using a role, build_dir and build_dir_extra are normally not set, + # since they are picked up from Labgrid-sjg via the u-boot-test-getrole + # script + board_identity = role + cmd = ['u-boot-test-getrole', role, '--configure'] + env = os.environ.copy() + if build_dir: + env['U_BOOT_BUILD_DIR'] = build_dir + if build_dir_extra: + env['U_BOOT_BUILD_DIR_EXTRA'] = build_dir_extra + proc = subprocess.run(cmd, capture_output=True, encoding='utf-8', + env=env) + if proc.returncode: + raise ValueError(proc.stderr) + # For debugging + # print('conftest: lab:', proc.stdout) + vals = {} + for line in proc.stdout.splitlines(): + item, value = line.split(' ', maxsplit=1) + k = item.split(':')[-1] + vals[k] = value + # For debugging + # print('conftest: lab info:', vals) + + # Read the build directories here, in case none were provided in the + # command-line arguments + (board_type, board_type_extra, default_build_dir, + default_build_dir_extra, source_dir) = (vals['board'], + vals['board_extra'], vals['build_dir'], vals['build_dir_extra'], + vals['source_dir']) + else: + board_type = config.getoption('board_type') + board_type_extra = config.getoption('board_type_extra') + board_identity = config.getoption('board_identity') + + source_dir = os.path.dirname(os.path.dirname(TEST_PY_DIR)) + default_build_dir = source_dir + '/build-' + board_type + default_build_dir_extra = source_dir + '/build-' + board_type_extra - source_dir = os.path.dirname(os.path.dirname(TEST_PY_DIR)) - default_build_dir = source_dir + '/build-' + board_type + # Use the provided command-line arguments if present, else fall back to if not build_dir: build_dir = default_build_dir + if not build_dir_extra: + build_dir_extra = default_build_dir_extra - return board_type, board_identity, build_dir, source_dir + return (board_type, board_type_extra, board_identity, build_dir, + build_dir_extra, source_dir) def pytest_xdist_setupnodes(config, specs): """Clear out any 'done' file from a previous build""" global build_done_file - build_dir = get_details(config)[2] + build_dir = get_details(config)[3] build_done_file = Path(build_dir) / 'build.done' if build_done_file.exists(): @@ -184,7 +237,8 @@ def pytest_configure(config): global console global ubconfig - board_type, board_identity, build_dir, source_dir = get_details(config) + (board_type, board_type_extra, board_identity, build_dir, build_dir_extra, + source_dir) = get_details(config) board_type_filename = board_type.replace('-', '_') board_identity_filename = board_identity.replace('-', '_') @@ -249,20 +303,25 @@ def pytest_configure(config): ubconfig.test_py_dir = TEST_PY_DIR ubconfig.source_dir = source_dir ubconfig.build_dir = build_dir + ubconfig.build_dir_extra = build_dir_extra ubconfig.result_dir = result_dir ubconfig.persistent_data_dir = persistent_data_dir ubconfig.board_type = board_type + ubconfig.board_type_extra = board_type_extra ubconfig.board_identity = board_identity ubconfig.gdbserver = gdbserver + ubconfig.use_running_system = config.getoption('use_running_system') ubconfig.dtb = build_dir + '/arch/sandbox/dts/test.dtb' ubconfig.connection_ok = True env_vars = ( 'board_type', + 'board_type_extra', 'board_identity', 'source_dir', 'test_py_dir', 'build_dir', + 'build_dir_extra', 'result_dir', 'persistent_data_dir', ) diff --git a/test/py/tests/test_spi.py b/test/py/tests/test_spi.py index caca9303271..0abdfa78b76 100644 --- a/test/py/tests/test_spi.py +++ b/test/py/tests/test_spi.py @@ -75,7 +75,7 @@ def get_params_spi(u_boot_console): ''' Get SPI device test parameters from boardenv file ''' f = u_boot_console.config.env.get('env__spi_device_test', None) if not f: - pytest.skip('No env file to read for SPI family device test') + pytest.skip('No SPI test device configured') bus = f.get('bus', 0) cs = f.get('chip_select', 0) @@ -84,7 +84,7 @@ def get_params_spi(u_boot_console): timeout = f.get('timeout', None) if not part_name: - pytest.skip('No env file to read SPI family flash part name') + pytest.skip('No SPI test device configured') return bus, cs, mode, part_name, timeout @@ -92,7 +92,7 @@ def spi_find_freq_range(u_boot_console): '''Find out minimum and maximum frequnecies that SPI device can operate''' f = u_boot_console.config.env.get('env__spi_device_test', None) if not f: - pytest.skip('No env file to read for SPI family device test') + pytest.skip('No SPI test device configured') min_f = f.get('min_freq', None) max_f = f.get('max_freq', None) @@ -116,21 +116,21 @@ def spi_pre_commands(u_boot_console, freq): pytest.fail('No SPI device available') if not part_name in output: - pytest.fail('SPI flash part name not recognized') + pytest.fail('Not recognized the SPI flash part name') m = re.search('page size (.+?) Bytes', output) if m: try: page_size = int(m.group(1)) except ValueError: - pytest.fail('SPI page size not recognized') + pytest.fail('Not recognized the SPI page size') m = re.search('erase size (.+?) KiB', output) if m: try: erase_size = int(m.group(1)) except ValueError: - pytest.fail('SPI erase size not recognized') + pytest.fail('Not recognized the SPI erase size') erase_size *= 1024 @@ -139,7 +139,7 @@ def spi_pre_commands(u_boot_console, freq): try: total_size = int(m.group(1)) except ValueError: - pytest.fail('SPI total size not recognized') + pytest.fail('Not recognized the SPI total size') total_size *= 1024 * 1024 @@ -149,7 +149,7 @@ def spi_pre_commands(u_boot_console, freq): flash_part = m.group(1) assert flash_part == part_name except ValueError: - pytest.fail('SPI flash part not recognized') + pytest.fail('Not recognized the SPI flash part') global SPI_DATA SPI_DATA = { @@ -574,7 +574,7 @@ def test_spi_lock_unlock(u_boot_console): min_f, max_f, loop = spi_find_freq_range(u_boot_console) flashes = u_boot_console.config.env.get('env__spi_lock_unlock', False) if not flashes: - pytest.skip('No supported flash list for lock/unlock provided') + pytest.skip('No SPI test device configured for lock/unlock') i = 0 while i < loop: @@ -695,7 +695,7 @@ def test_spi_negative(u_boot_console): # Read to relocation address output = u_boot_console.run_command('bdinfo') - m = re.search('relocaddr\s*= (.+)', output) + m = re.search(r'relocaddr\s*= (.+)', output) res_area = int(m.group(1), 16) start = 0 diff --git a/test/py/u_boot_console_base.py b/test/py/u_boot_console_base.py index d8d0bdf9fd4..fa9cd57b04b 100644 --- a/test/py/u_boot_console_base.py +++ b/test/py/u_boot_console_base.py @@ -23,12 +23,22 @@ pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ') pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'') pattern_error_notification = re.compile('## Error: ') pattern_error_please_reset = re.compile('### ERROR ### Please RESET the board ###') +pattern_ready_prompt = re.compile('{lab ready in (.*)s: (.*)}') +pattern_lab_mode = re.compile('{lab mode.*}') PAT_ID = 0 PAT_RE = 1 # Timeout before expecting the console to be ready (in milliseconds) -TIMEOUT_MS = 30000 +TIMEOUT_MS = 30000 # Standard timeout +TIMEOUT_CMD_MS = 10000 # Command-echo timeout + +# Timeout for board preparation in lab mode. This needs to be enough to build +# U-Boot, write it to the board and then boot the board. Since this process is +# under the control of another program (e.g. Labgrid), it will failure sooner +# if something goes way. So use a very long timeout here to cover all possible +# situations. +TIMEOUT_PREPARE_MS = 3 * 60 * 1000 bad_pattern_defs = ( ('spl_signon', pattern_u_boot_spl_signon), @@ -142,6 +152,7 @@ class ConsoleBase(object): self.at_prompt = False self.at_prompt_logevt = None + self.lab_mode = False def get_spawn(self): # This is not called, ssubclass must define this. @@ -172,43 +183,75 @@ class ConsoleBase(object): """ if self.p: - self.p.close() + self.log.start_section('Stopping U-Boot') + close_type = self.p.close() + self.log.info(f'Close type: {close_type}') + self.log.end_section('Stopping U-Boot') self.logstream.close() + def set_lab_mode(self): + """Select lab mode + + This tells us that we will get a 'lab ready' message when the board is + ready for use. We don't need to look for signon messages. + """ + self.log.info(f'test.py: Lab mode is active') + self.p.timeout = TIMEOUT_PREPARE_MS + self.lab_mode = True + def wait_for_boot_prompt(self, loop_num = 1): """Wait for the boot up until command prompt. This is for internal use only. """ try: + self.log.info('Waiting for U-Boot to be ready') bcfg = self.config.buildconfig config_spl_serial = bcfg.get('config_spl_serial', 'n') == 'y' env_spl_skipped = self.config.env.get('env__spl_skipped', False) env_spl_banner_times = self.config.env.get('env__spl_banner_times', 1) - while loop_num > 0: + while not self.lab_mode and loop_num > 0: loop_num -= 1 while config_spl_serial and not env_spl_skipped and env_spl_banner_times > 0: - m = self.p.expect([pattern_u_boot_spl_signon] + - self.bad_patterns) - if m != 0: + m = self.p.expect([pattern_u_boot_spl_signon, + pattern_lab_mode] + self.bad_patterns) + if m == 1: + self.set_lab_mode() + break + elif m != 0: raise BootFail('Bad pattern found on SPL console: ' + - self.bad_pattern_ids[m - 1]) + self.bad_pattern_ids[m - 1]) env_spl_banner_times -= 1 - m = self.p.expect([pattern_u_boot_main_signon] + self.bad_patterns) - if m != 0: - raise BootFail('Bad pattern found on console: ' + - self.bad_pattern_ids[m - 1]) - self.u_boot_version_string = self.p.after + if not self.lab_mode: + m = self.p.expect([pattern_u_boot_main_signon, + pattern_lab_mode] + self.bad_patterns) + if m == 1: + self.set_lab_mode() + elif m != 0: + raise BootFail('Bad pattern found on console: ' + + self.bad_pattern_ids[m - 1]) + if not self.lab_mode: + self.u_boot_version_string = self.p.after while True: - m = self.p.expect([self.prompt_compiled, + m = self.p.expect([self.prompt_compiled, pattern_ready_prompt, pattern_stop_autoboot_prompt] + self.bad_patterns) if m == 0: + self.log.info(f'Found ready prompt {m}') break - if m == 1: + elif m == 1: + m = pattern_ready_prompt.search(self.p.after) + self.u_boot_version_string = m.group(2) + self.log.info(f'Lab: Board is ready') + self.p.timeout = TIMEOUT_MS + break + if m == 2: + self.log.info(f'Found autoboot prompt {m}') self.p.send(' ') continue - raise BootFail('Bad pattern found on console: ' + - self.bad_pattern_ids[m - 2]) + if not self.lab_mode: + raise BootFail('Missing prompt / ready message on console: ' + + self.bad_pattern_ids[m - 3]) + self.log.info(f'U-Boot is ready') finally: self.log.timestamp() @@ -261,22 +304,28 @@ class ConsoleBase(object): try: self.at_prompt = False + if not self.p: + raise BootFail( + f"Lab failure: Connection lost when sending command '{cmd}'") + if send_nl: cmd += '\n' - while cmd: - # Limit max outstanding data, so UART FIFOs don't overflow - chunk = cmd[:self.max_fifo_fill] - cmd = cmd[self.max_fifo_fill:] - self.p.send(chunk) - if not wait_for_echo: - continue - chunk = re.escape(chunk) - chunk = chunk.replace('\\\n', '[\r\n]') - m = self.p.expect([chunk] + self.bad_patterns) - if m != 0: - self.at_prompt = False - raise BootFail('Bad pattern found on console: ' + - self.bad_pattern_ids[m - 1]) + rem = cmd # Remaining to be sent + with self.temporary_timeout(TIMEOUT_CMD_MS): + while rem: + # Limit max outstanding data, so UART FIFOs don't overflow + chunk = rem[:self.max_fifo_fill] + rem = rem[self.max_fifo_fill:] + self.p.send(chunk) + if not wait_for_echo: + continue + chunk = re.escape(chunk) + chunk = chunk.replace('\\\n', '[\r\n]') + m = self.p.expect([chunk] + self.bad_patterns) + if m != 0: + self.at_prompt = False + raise BootFail(f"Failed to get echo on console (cmd '{cmd}':rem '{rem}'): " + + self.bad_pattern_ids[m - 1]) if not wait_for_prompt: return if wait_for_reboot: @@ -440,11 +489,17 @@ class ConsoleBase(object): if not self.config.gdbserver: self.p.timeout = TIMEOUT_MS self.p.logfile_read = self.logstream - if expect_reset: - loop_num = 2 + if self.config.use_running_system: + # Send an empty command to set up the 'expect' logic. This has + # the side effect of ensuring that there was no partial command + # line entered + self.run_command(' ') else: - loop_num = 1 - self.wait_for_boot_prompt(loop_num = loop_num) + if expect_reset: + loop_num = 2 + else: + loop_num = 1 + self.wait_for_boot_prompt(loop_num = loop_num) self.at_prompt = True self.at_prompt_logevt = self.logstream.logfile.cur_evt except Exception as ex: diff --git a/test/py/u_boot_console_exec_attach.py b/test/py/u_boot_console_exec_attach.py index 8dd8cc1230c..8b253b4451d 100644 --- a/test/py/u_boot_console_exec_attach.py +++ b/test/py/u_boot_console_exec_attach.py @@ -59,14 +59,27 @@ class ConsoleExecAttach(ConsoleBase): args = [self.config.board_type, self.config.board_identity] s = Spawn(['u-boot-test-console'] + args) - try: - self.log.action('Resetting board') - cmd = ['u-boot-test-reset'] + args - runner = self.log.get_runner(cmd[0], sys.stdout) - runner.run(cmd) - runner.close() - except: - s.close() - raise + if self.config.use_running_system: + self.log.action('Connecting to board without reset') + else: + try: + self.log.action('Resetting board') + cmd = ['u-boot-test-reset'] + args + runner = self.log.get_runner(cmd[0], sys.stdout) + runner.run(cmd) + runner.close() + except: + s.close() + raise return s + + def close(self): + super().close() + + self.log.action('Releasing board') + args = [self.config.board_type, self.config.board_identity] + cmd = ['u-boot-test-release'] + args + runner = self.log.get_runner(cmd[0], sys.stdout) + runner.run(cmd) + runner.close() diff --git a/test/py/u_boot_spawn.py b/test/py/u_boot_spawn.py index 24d369035e5..c703454389d 100644 --- a/test/py/u_boot_spawn.py +++ b/test/py/u_boot_spawn.py @@ -5,15 +5,21 @@ Logic to spawn a sub-process and interact with its stdio. """ +import io import os import re import pty import pytest import signal import select +import sys +import termios import time import traceback +# Character to send (twice) to exit the terminal +EXIT_CHAR = 0x1d # FS (Ctrl + ]) + class Timeout(Exception): """An exception sub-class that indicates that a timeout occurred.""" @@ -115,11 +121,30 @@ class Spawn: finally: os._exit(255) + old = None try: + isatty = False + try: + isatty = os.isatty(sys.stdout.fileno()) + + # with --capture=tee-sys we cannot call fileno() + except io.UnsupportedOperation as exc: + pass + if isatty: + new = termios.tcgetattr(self.fd) + old = new + new[3] = new[3] & ~(termios.ICANON | termios.ISIG) + new[3] = new[3] & ~termios.ECHO + new[6][termios.VMIN] = 0 + new[6][termios.VTIME] = 0 + termios.tcsetattr(self.fd, termios.TCSANOW, new) + self.poll = select.poll() self.poll.register(self.fd, select.POLLIN | select.POLLPRI | select.POLLERR | select.POLLHUP | select.POLLNVAL) except: + if old: + termios.tcsetattr(self.fd, termios.TCSANOW, old) self.close() raise @@ -289,15 +314,28 @@ class Spawn: None. Returns: - Nothing. + str: Type of closure completed """ - + # For Labgrid-sjg, ask it is exit gracefully, so it can transition the + # board to the final state (like 'off') before exiting. + if os.environ.get('USE_LABGRID_SJG'): + self.send(chr(EXIT_CHAR) * 2) + + # Wait about 10 seconds for Labgrid to close and power off the board + for _ in range(100): + if not self.isalive(): + return 'normal' + time.sleep(0.1) + + # That didn't work, so try closing the PTY os.close(self.fd) for _ in range(100): if not self.isalive(): - break + return 'break' time.sleep(0.1) + return 'timeout' + def get_expect_output(self): """Return the output read by expect() |