Skip to content

Commit

Permalink
pytest: gdbstub: Add GDB helper to Twister pytest plugin
Browse files Browse the repository at this point in the history
Add GDB helper and fixture to Twister pytest plugin to reuse
it for test cases where GDB execution is needed.

Adjust GDB stub test to use the new helper class.

Signed-off-by: Dmitrii Golovanov <dmitrii.golovanov@intel.com>
  • Loading branch information
golowanow committed Oct 13, 2023
1 parent 66ed85c commit 217aee8
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
# SPDX-License-Identifier: Apache-2.0

import logging
from typing import Generator, Type
from subprocess import CompletedProcess
from typing import Generator, Type, Callable

import pytest

Expand All @@ -12,6 +13,7 @@
from twister_harness.twister_harness_config import DeviceConfig, TwisterHarnessConfig
from twister_harness.helpers.shell import Shell
from twister_harness.helpers.mcumgr import MCUmgr
from twister_harness.helpers.gdb import GDBrunner

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -66,3 +68,14 @@ def is_mcumgr_available() -> None:
@pytest.fixture()
def mcumgr(is_mcumgr_available: None, dut: DeviceAdapter) -> Generator[MCUmgr, None, None]:
yield MCUmgr.create_for_serial(dut.device_config.serial)


@pytest.fixture(scope='function')
def gdb_batch_make(dut: DeviceAdapter) -> Callable[[DeviceAdapter, str, str, float], CompletedProcess]:
"""Returns GDBrunner factory method to call in a test context"""
def batch_make(dut: DeviceAdapter, gdb_target_remote: str, gdb_script: str, gdb_timeout: float) -> CompletedProcess:
gdb = GDBrunner(dut)

Check warning on line 77 in scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

W0311

scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py:77 Bad indentation. Found 6 spaces, expected 8 (bad-indentation)
return gdb.run_script(gdb_target_remote, gdb_script, gdb_timeout)

Check warning on line 78 in scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

W0311

scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py:78 Bad indentation. Found 6 spaces, expected 8 (bad-indentation)
#
return batch_make
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) 2023 Intel Corporation.
#
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import os
import subprocess
import sys
import logging
import shlex

from twister_harness.device.device_adapter import DeviceAdapter

ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "pylib", "twister"))
from twisterlib.cmakecache import CMakeCache

logger = logging.getLogger(__name__)


class GDBrunner:
"""
Helper class to run GDB process as a part of the test.
"""

def __init__(self, device: DeviceAdapter) -> None:
build_dir = device.device_config.build_dir
cmake_cache = CMakeCache.from_file(os.path.join(build_dir, 'CMakeCache.txt'))

gdb_exec = cmake_cache.get('CMAKE_GDB', None)
assert gdb_exec

self.source_dir = cmake_cache.get('APPLICATION_SOURCE_DIR', None)
assert self.source_dir

self.build_image = cmake_cache.get('BYPRODUCT_KERNEL_ELF_NAME', None)
assert self.build_image

gdb_log_file = os.path.join(build_dir, 'gdb.log')

self.command = [gdb_exec, '-batch',
'-ex', f'set pagination off',
'-ex', f'set trace-commands on',
'-ex', f'set logging file {gdb_log_file}',
'-ex', f'set logging enabled on'
]
#

def run_script(self, target_remote: str, gdb_script:str, timeout:float | None = None) -> subprocess.CompletedProcess:
"""
Run GDB process which executes a script connecting to remote target backend.
"""
cmd = self.command + [
'-ex', f'target remote {target_remote}',
'-x', f'{self.source_dir}/{gdb_script}',
self.build_image
]
logger.info(f'Run GDB: {shlex.join(cmd)}')
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
logger.info(f'GDB ends rc={result.returncode}')
return result

# GDBrunner
2 changes: 1 addition & 1 deletion tests/subsys/debug/gdbstub/pytest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def gdb_script(request):

@pytest.fixture()
def gdb_timeout(request):
return int(request.config.getoption('--gdb_timeout', default=60))
return float(request.config.getoption('--gdb_timeout', default=60))

@pytest.fixture()
def gdb_target_remote(request):
Expand Down
47 changes: 10 additions & 37 deletions tests/subsys/debug/gdbstub/pytest/test_gdbstub.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,14 @@
#
# SPDX-License-Identifier: Apache-2.0

import os
import subprocess
import sys
from subprocess import CompletedProcess
import logging
import shlex
import re
import pytest
from twister_harness import DeviceAdapter

ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "pylib", "twister"))
from twisterlib.cmakecache import CMakeCache

logger = logging.getLogger(__name__)

@pytest.fixture()
def gdb_process(dut: DeviceAdapter, gdb_script, gdb_timeout, gdb_target_remote):
build_dir = dut.device_config.build_dir
cmake_cache = CMakeCache.from_file(os.path.join(build_dir, 'CMakeCache.txt'))
gdb_exec = cmake_cache.get('CMAKE_GDB', None)
assert gdb_exec
source_dir = cmake_cache.get('APPLICATION_SOURCE_DIR', None)
assert source_dir
build_image = cmake_cache.get('BYPRODUCT_KERNEL_ELF_NAME', None)
assert build_image
gdb_log_file = os.path.join(build_dir, 'gdb.log')
cmd = [gdb_exec, '-batch',
'-ex', f'set pagination off',
'-ex', f'set trace-commands on',
'-ex', f'set logging file {gdb_log_file}',
'-ex', f'set logging enabled on',
'-ex', f'target remote {gdb_target_remote}',
'-x', f'{source_dir}/{gdb_script}', build_image]
logger.info(f'Run GDB: {shlex.join(cmd)}')
result = subprocess.run(cmd, capture_output=True, text=True, timeout=gdb_timeout)
logger.info(f'GDB ends rc={result.returncode}')
return result
#

@pytest.fixture(scope="module")
def expected_app():
return [
Expand All @@ -66,17 +35,21 @@ def expected_gdb_detach():
re.compile(r'Inferior.*detached') # QEMU gdbstub
]

@pytest.fixture(scope='function')
def gdb_batch(dut: DeviceAdapter, gdb_batch_make, gdb_target_remote, gdb_script, gdb_timeout) -> CompletedProcess:
return gdb_batch_make(dut, gdb_target_remote, gdb_script, gdb_timeout)


def test_gdbstub(dut: DeviceAdapter, gdb_process, expected_app, expected_gdb, expected_gdb_detach):
def test_gdbstub(dut: DeviceAdapter, gdb_batch: CompletedProcess, expected_app, expected_gdb, expected_gdb_detach):
"""
Test gdbstub feature using a GDB script. We connect to the DUT, run the
GDB script then evaluate return code and expected patterns at the GDB
and Test Applicaiton outputs.
"""
logger.debug(f"GDB output:\n{gdb_process.stdout}\n")
assert gdb_process.returncode == 0
assert all([ex_re.search(gdb_process.stdout, re.MULTILINE) for ex_re in expected_gdb]), 'No expected GDB output'
assert any([ex_re.search(gdb_process.stdout, re.MULTILINE) for ex_re in expected_gdb_detach]), 'No expected GDB quit'
logger.debug(f"GDB output:\n{gdb_batch.stdout}\n")
assert gdb_batch.returncode == 0
assert all([ex_re.search(gdb_batch.stdout, re.MULTILINE) for ex_re in expected_gdb]), 'No expected GDB output'
assert any([ex_re.search(gdb_batch.stdout, re.MULTILINE) for ex_re in expected_gdb_detach]), 'No expected GDB quit'
app_output = '\n'.join(dut.readlines(print_output = False))
logger.debug(f"App output:\n{app_output}\n")
assert all([ex_re.search(app_output, re.MULTILINE) for ex_re in expected_app]), 'No expected Application output'
Expand Down

0 comments on commit 217aee8

Please sign in to comment.