Skip to content

Commit d80e3f7

Browse files
nashifkartben
authored andcommitted
twister: harness: introduce shell harness
Introduce a new harness based on pytest that does basic shell command handling. The harness is enabeld using: harness: shell and expects a file with parameters in the form: test_shell_harness: - command: "kernel version" expected: "Zephyr version .*" - ... Multiple commands and their expected output can be tested. Signed-off-by: Anas Nashif <[email protected]>
1 parent 4fc6f12 commit d80e3f7

File tree

6 files changed

+72
-2
lines changed

6 files changed

+72
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) 2025 Intel Corporation
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
6+
def pytest_addoption(parser):
7+
parser.addoption('--testdata')
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) 2025 Intel Corporation
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
import logging
5+
import re
6+
7+
import pytest
8+
import yaml
9+
from twister_harness import Shell
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
@pytest.fixture
15+
def testdata_path(request):
16+
return request.config.getoption("--testdata")
17+
18+
19+
def get_next_commands(testdata_path):
20+
with open(testdata_path) as yaml_file:
21+
data = yaml.safe_load(yaml_file)
22+
for entry in data['test_shell_harness']:
23+
yield entry['command'], entry['expected']
24+
25+
26+
def test_shell_harness(shell: Shell, testdata_path):
27+
for command, expected in get_next_commands(testdata_path):
28+
logger.info('send command: %s', command)
29+
lines = shell.exec_command(command)
30+
match = False
31+
for line in lines:
32+
if re.match(expected, line):
33+
match = True
34+
break
35+
assert match, 'expected response not found'
36+
logger.info('response is valid')

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ def generate_command(self):
403403
f'--junit-xml={self.report_file}',
404404
f'--platform={self.instance.platform.name}'
405405
]
406+
406407
command.extend([os.path.normpath(os.path.join(
407408
self.source_dir, os.path.expanduser(os.path.expandvars(src)))) for src in pytest_root])
408409

@@ -627,6 +628,19 @@ def _parse_report_file(self, report):
627628
self.status = TwisterStatus.SKIP
628629
self.instance.reason = 'No tests collected'
629630

631+
class Shell(Pytest):
632+
def generate_command(self):
633+
config = self.instance.testsuite.harness_config
634+
pytest_root = [os.path.join(ZEPHYR_BASE, 'scripts', 'pylib', 'shell-twister-harness')]
635+
config['pytest_root'] = pytest_root
636+
637+
command = super().generate_command()
638+
if config.get('shell_params_file'):
639+
p_file = os.path.join(self.source_dir, config.get('shell_params_file'))
640+
command.append(f'--testdata={p_file}')
641+
else:
642+
command.append(f'--testdata={os.path.join(self.source_dir, "test_shell.yml")}')
643+
return command
630644

631645
class Gtest(Harness):
632646
ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,16 @@ def get_case_or_create(self, name):
218218
def testsuite_runnable(testsuite, fixtures):
219219
can_run = False
220220
# console harness allows us to run the test and capture data.
221-
if testsuite.harness in ['console', 'ztest', 'pytest', 'test', 'gtest', 'robot', 'ctest']:
221+
if testsuite.harness in [
222+
'console',
223+
'ztest',
224+
'pytest',
225+
'test',
226+
'gtest',
227+
'robot',
228+
'ctest',
229+
'shell'
230+
]:
222231
can_run = True
223232
# if we have a fixture that is also being supplied on the
224233
# command-line, then we need to run the test, not just build it.
@@ -304,7 +313,7 @@ def check_runnable(self,
304313
device_testing)
305314

306315
# check if test is runnable in pytest
307-
if self.testsuite.harness == 'pytest':
316+
if self.testsuite.harness in ['pytest', 'shell']:
308317
target_ready = bool(
309318
filter == 'runnable' or simulator and simulator.name in SUPPORTED_SIMS_IN_PYTEST
310319
)

scripts/schemas/twister/testsuite-schema.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ schema;scenario-schema:
107107
type: map
108108
required: false
109109
mapping:
110+
"shell_params_file":
111+
type: str
112+
required: false
110113
"type":
111114
type: str
112115
required: false

scripts/tests/twister/pytest_integration/test_harness_pytest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
def testinstance() -> TestInstance:
1919
testsuite = TestSuite('.', 'samples/hello', 'unit.test')
2020
testsuite.harness_config = {}
21+
testsuite.harness = 'pytest'
2122
testsuite.ignore_faults = False
2223
testsuite.sysbuild = False
2324
platform = Platform()

0 commit comments

Comments
 (0)