Skip to content

Commit 51cba9d

Browse files
gchwiercfriedt
authored andcommitted
twister: Add unit tests for required applications
Added tests for sharing of build application feature added in #94167 Signed-off-by: Grzegorz Chwierut <[email protected]>
1 parent 4a4a94b commit 51cba9d

File tree

4 files changed

+248
-8
lines changed

4 files changed

+248
-8
lines changed

scripts/tests/twister/conftest.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,20 @@ def tesenv_obj(test_data, testsuites_dir, tmpdir_factory):
4747
options.detailed_test_id = True
4848
env = TwisterEnv(options)
4949
env.board_roots = [os.path.join(test_data, "board_config", "1_level", "2_level")]
50-
env.test_roots = [os.path.join(testsuites_dir, 'tests', testsuites_dir, 'samples')]
50+
env.test_roots = [os.path.join(testsuites_dir, 'tests'),
51+
os.path.join(testsuites_dir, 'samples')]
5152
env.test_config = os.path.join(test_data, "test_config.yaml")
5253
env.outdir = tmpdir_factory.mktemp("sanity_out_demo")
5354
return env
5455

5556

5657
@pytest.fixture(name='class_testplan')
57-
def testplan_obj(test_data, class_env, testsuites_dir, tmpdir_factory):
58+
def testplan_obj(class_env):
5859
""" Pytest fixture to initialize and return the class TestPlan object"""
5960
env = class_env
60-
env.board_roots = [test_data +"board_config/1_level/2_level/"]
61-
env.test_roots = [testsuites_dir + '/tests', testsuites_dir + '/samples']
62-
env.outdir = tmpdir_factory.mktemp("sanity_out_demo")
6361
plan = TestPlan(env)
6462
plan.test_config = TestConfiguration(config_file=env.test_config)
63+
plan.options.outdir = env.outdir
6564
return plan
6665

6766
@pytest.fixture(name='all_testsuites_dict')
@@ -84,15 +83,14 @@ def all_platforms_list(test_data, class_testplan):
8483
return plan.platforms
8584

8685
@pytest.fixture
87-
def instances_fixture(class_testplan, platforms_list, all_testsuites_dict, tmpdir_factory):
86+
def instances_fixture(class_testplan, platforms_list, all_testsuites_dict):
8887
""" Pytest fixture to call add_instances function of Testsuite class
8988
and return the instances dictionary"""
90-
class_testplan.outdir = tmpdir_factory.mktemp("sanity_out_demo")
9189
class_testplan.platforms = platforms_list
9290
platform = class_testplan.get_platform("demo_board_2")
9391
instance_list = []
9492
for _, testcase in all_testsuites_dict.items():
95-
instance = TestInstance(testcase, platform, 'zephyr', class_testplan.outdir)
93+
instance = TestInstance(testcase, platform, 'zephyr', class_testplan.env.outdir)
9694
instance_list.append(instance)
9795
class_testplan.add_instances(instance_list)
9896
return class_testplan.instances

scripts/tests/twister/pytest_integration/test_harness_pytest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ def test_pytest_command_extra_args_in_options(testinstance: TestInstance):
9797
assert command.index(pytest_args_from_yaml) < command.index(pytest_args_from_cmd[1])
9898

9999

100+
def test_pytest_command_required_build_args(testinstance: TestInstance):
101+
""" Test that required build dirs are passed to pytest harness """
102+
pytest_harness = Pytest()
103+
required_builds = ['/req/build/dir', 'another/req/dir']
104+
testinstance.required_build_dirs = required_builds
105+
pytest_harness.configure(testinstance)
106+
command = pytest_harness.generate_command()
107+
for req_dir in required_builds:
108+
assert f'--required-build={req_dir}' in command
109+
110+
100111
@pytest.mark.parametrize(
101112
('pytest_root', 'expected'),
102113
[

scripts/tests/twister/test_runner.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2853,3 +2853,134 @@ def test_twisterrunner_get_cmake_filter_stages(filter, expected_result):
28532853
result = TwisterRunner.get_cmake_filter_stages(filter, ['not', 'and'])
28542854

28552855
assert sorted(result) == sorted(expected_result)
2856+
2857+
2858+
@pytest.mark.parametrize(
2859+
'required_apps, processing_ready_keys, expected_result',
2860+
[
2861+
(['app1', 'app2'], ['app1', 'app2'], True), # all apps ready
2862+
(['app1', 'app2', 'app3'], ['app1', 'app2'], False), # some apps missing
2863+
([], [], True), # no required apps
2864+
(['app1'], [], False), # single app missing
2865+
],
2866+
ids=['all_ready', 'some_missing', 'no_apps', 'single_missing']
2867+
)
2868+
def test_twisterrunner_are_required_apps_ready(required_apps, processing_ready_keys, expected_result):
2869+
"""Test _are_required_apps_ready method with various scenarios"""
2870+
instances = {}
2871+
suites = []
2872+
env_mock = mock.Mock()
2873+
tr = TwisterRunner(instances, suites, env=env_mock)
2874+
2875+
instance_mock = mock.Mock()
2876+
instance_mock.required_applications = required_apps
2877+
2878+
processing_ready = {key: mock.Mock() for key in processing_ready_keys}
2879+
2880+
result = tr._are_required_apps_ready(instance_mock, processing_ready)
2881+
2882+
assert result is expected_result
2883+
2884+
2885+
@pytest.mark.parametrize(
2886+
'app_statuses, expected_result',
2887+
[
2888+
([TwisterStatus.PASS, TwisterStatus.PASS], True), # all passed
2889+
([TwisterStatus.NOTRUN, TwisterStatus.NOTRUN], True), # all notrun
2890+
([TwisterStatus.PASS, TwisterStatus.NOTRUN], True), # mixed pass/notrun
2891+
([TwisterStatus.PASS, TwisterStatus.FAIL], False), # one failed
2892+
([TwisterStatus.ERROR], False), # single error
2893+
],
2894+
ids=['all_pass', 'all_notrun', 'mixed_pass_notrun', 'one_fail', 'single_error']
2895+
)
2896+
def test_twisterrunner_are_all_required_apps_success(app_statuses, expected_result):
2897+
"""Test _are_all_required_apps_success method with various app statuses"""
2898+
instances = {}
2899+
suites = []
2900+
env_mock = mock.Mock()
2901+
tr = TwisterRunner(instances, suites, env=env_mock)
2902+
2903+
instance_mock = mock.Mock()
2904+
required_apps = [f'app{i + 1}' for i in range(len(app_statuses))]
2905+
instance_mock.required_applications = required_apps
2906+
2907+
processing_ready = {}
2908+
for i, status in enumerate(app_statuses):
2909+
app_instance = mock.Mock()
2910+
app_instance.status = status
2911+
app_instance.reason = f"Reason for app{i + 1}"
2912+
processing_ready[f'app{i + 1}'] = app_instance
2913+
2914+
result = tr._are_all_required_apps_success(instance_mock, processing_ready)
2915+
assert result is expected_result
2916+
2917+
2918+
@pytest.mark.parametrize(
2919+
'required_apps, ready_apps, expected_result, expected_actions',
2920+
[
2921+
([], {}, True,
2922+
{'requeue': False, 'skip': False, 'build_dirs': 0}),
2923+
(['app1'], {}, False,
2924+
{'requeue': True, 'skip': False, 'build_dirs': 0}),
2925+
(['app1', 'app2'], {'app1': TwisterStatus.PASS}, False,
2926+
{'requeue': True, 'skip': False, 'build_dirs': 0}),
2927+
(['app1'], {'app1': TwisterStatus.FAIL}, False,
2928+
{'requeue': False, 'skip': True, 'build_dirs': 0}),
2929+
(['app1', 'app2'], {'app1': TwisterStatus.PASS, 'app2': TwisterStatus.NOTRUN}, True,
2930+
{'requeue': False, 'skip': False, 'build_dirs': 2}),
2931+
],
2932+
ids=['no_apps', 'not_ready_single_job', 'not_ready_multi_job',
2933+
'apps_failed', 'apps_success']
2934+
)
2935+
def test_twisterrunner_are_required_apps_processed(required_apps, ready_apps,
2936+
expected_result, expected_actions):
2937+
"""Test are_required_apps_processed method with various scenarios"""
2938+
# Setup TwisterRunner instances dict
2939+
tr_instances = {}
2940+
for app_name in required_apps:
2941+
tr_instances[app_name] = mock.Mock(build_dir=f'/path/to/{app_name}')
2942+
2943+
env_mock = mock.Mock()
2944+
tr = TwisterRunner(tr_instances, [], env=env_mock)
2945+
tr.jobs = 1
2946+
2947+
instance_mock = mock.Mock()
2948+
instance_mock.required_applications = required_apps[:]
2949+
instance_mock.required_build_dirs = []
2950+
2951+
# Setup testcases for skip scenarios
2952+
if expected_actions['skip']:
2953+
testcase_mock = mock.Mock()
2954+
instance_mock.testcases = [testcase_mock]
2955+
2956+
# Setup processing_ready with app instances
2957+
processing_ready = {}
2958+
for app_name, status in ready_apps.items():
2959+
app_instance = mock.Mock()
2960+
app_instance.status = status
2961+
app_instance.reason = f"Reason for {app_name}"
2962+
app_instance.build_dir = f'/path/to/{app_name}'
2963+
processing_ready[app_name] = app_instance
2964+
2965+
processing_queue = deque()
2966+
task = {'test': instance_mock}
2967+
2968+
result = tr.are_required_apps_processed(instance_mock, processing_queue, processing_ready, task)
2969+
2970+
assert result is expected_result
2971+
2972+
if expected_actions['requeue']:
2973+
assert len(processing_queue) == 1
2974+
assert processing_queue[0] == task
2975+
2976+
if expected_actions['skip']:
2977+
assert instance_mock.status == TwisterStatus.SKIP
2978+
assert instance_mock.reason == "Required application failed"
2979+
assert instance_mock.required_applications == []
2980+
assert instance_mock.testcases[0].status == TwisterStatus.SKIP
2981+
# Check for report task in queue
2982+
assert any(item.get('op') == 'report' for item in processing_queue)
2983+
2984+
assert len(instance_mock.required_build_dirs) == expected_actions['build_dirs']
2985+
if expected_actions['build_dirs'] > 0:
2986+
assert instance_mock.required_applications == []

scripts/tests/twister/test_testplan.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
from contextlib import nullcontext
15+
from pathlib import Path
1516

1617
ZEPHYR_BASE = os.getenv("ZEPHYR_BASE")
1718
sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/pylib/twister"))
@@ -252,6 +253,105 @@ def test_apply_filters_part3(class_testplan, all_testsuites_dict, platforms_list
252253
filtered_instances = list(filter(lambda item: item.status == TwisterStatus.FILTER, class_testplan.instances.values()))
253254
assert not filtered_instances
254255

256+
257+
def get_testsuite_for_given_test(plan: TestPlan, testname: str) -> TestSuite | None:
258+
""" Helper function to get testsuite object for a given testname"""
259+
for _, testsuite in plan.testsuites.items():
260+
if testname in testsuite.name:
261+
return testsuite
262+
return None
263+
264+
265+
@pytest.fixture()
266+
def testplan_with_one_instance(
267+
class_testplan: TestPlan, platforms_list, all_testsuites_dict
268+
) -> TestPlan:
269+
""" Pytest fixture to initialize and return the class TestPlan object
270+
with one instance for 'sample_test.app' test on 'demo_board_1' platform"""
271+
class_testplan.platforms = platforms_list
272+
class_testplan.platform_names = [p.name for p in platforms_list]
273+
class_testplan.testsuites = all_testsuites_dict
274+
platform = class_testplan.get_platform("demo_board_1")
275+
testsuite = get_testsuite_for_given_test(class_testplan, 'sample_test.app')
276+
testinstance = TestInstance(testsuite, platform, 'zephyr', class_testplan.env.outdir)
277+
class_testplan.add_instances([testinstance])
278+
return class_testplan
279+
280+
281+
def test_apply_changes_for_required_applications(testplan_with_one_instance: TestPlan):
282+
""" Testing apply_changes_for_required_applications function of TestPlan class in Twister """
283+
plan = testplan_with_one_instance
284+
testinstance_req = next(iter(plan.instances.values()))
285+
286+
testsuite = get_testsuite_for_given_test(plan, 'test_a.check_1')
287+
testsuite.required_applications = [{'name': 'sample_test.app'}]
288+
platform = plan.get_platform("demo_board_1")
289+
testinstance = TestInstance(testsuite, platform, 'zephyr', plan.env.outdir)
290+
plan.add_instances([testinstance])
291+
292+
plan.apply_changes_for_required_applications()
293+
# Check that the required application was added to the instance
294+
assert testinstance.required_applications[0] == testinstance_req.name
295+
296+
297+
def test_apply_changes_for_required_applications_missing_app(testplan_with_one_instance: TestPlan):
298+
""" Test apply_changes_for_required_applications when required application is missing """
299+
plan = testplan_with_one_instance
300+
testsuite = get_testsuite_for_given_test(plan, 'test_a.check_1')
301+
# Set a required application that does not exist
302+
testsuite.required_applications = [{'name': 'nonexistent_app'}]
303+
platform = plan.get_platform("demo_board_1")
304+
testinstance = TestInstance(testsuite, platform, 'zephyr', plan.env.outdir)
305+
plan.add_instances([testinstance])
306+
307+
plan.apply_changes_for_required_applications()
308+
# Check that the instance was filtered
309+
assert testinstance.status == TwisterStatus.FILTER
310+
assert "Missing required application" in testinstance.reason
311+
assert len(testinstance.required_applications) == 0
312+
313+
314+
def test_apply_changes_for_required_applications_wrong_platform(testplan_with_one_instance: TestPlan):
315+
""" Test apply_changes_for_required_applications with not matched platform """
316+
plan = testplan_with_one_instance
317+
testsuite = get_testsuite_for_given_test(plan, 'test_a.check_1')
318+
testsuite.required_applications = [{'name': 'sample_test.app', 'platform': 'demo_board_2'}]
319+
platform = plan.get_platform("demo_board_2")
320+
testinstance = TestInstance(testsuite, platform, 'zephyr', plan.env.outdir)
321+
plan.add_instances([testinstance])
322+
323+
plan.apply_changes_for_required_applications()
324+
# Check that the instance was filtered
325+
assert testinstance.status == TwisterStatus.FILTER
326+
assert "Missing required application" in testinstance.reason
327+
assert len(testinstance.required_applications) == 0
328+
329+
330+
def test_apply_changes_for_required_applications_in_outdir(testplan_with_one_instance: TestPlan):
331+
""" Testing apply_changes_for_required_applications when required application is already in outdir
332+
and --no-clean option is used """
333+
plan = testplan_with_one_instance
334+
plan.options.no_clean = True
335+
req_app_in_outdir = "prebuilt_sample_test.app"
336+
337+
testsuite = get_testsuite_for_given_test(plan, 'test_a.check_1')
338+
testsuite.required_applications = [{'name': req_app_in_outdir}]
339+
platform = plan.get_platform("demo_board_1")
340+
testinstance = TestInstance(testsuite, platform, 'zephyr', plan.env.outdir)
341+
plan.add_instances([testinstance])
342+
343+
# create the required application directory in outdir to simulate prebuilt app
344+
req_app_dir = Path(plan.env.outdir) / platform.normalized_name / "test_dir" / req_app_in_outdir
345+
(req_app_dir / "zephyr").mkdir(parents=True, exist_ok=True)
346+
347+
plan.apply_changes_for_required_applications()
348+
# Check that the required application was not added to the instance,
349+
# but the required build dir was added
350+
assert len(testinstance.required_applications) == 0
351+
assert len(testinstance.required_build_dirs) == 1
352+
assert str(req_app_dir) in testinstance.required_build_dirs
353+
354+
255355
def test_add_instances_short(tmp_path, class_env, all_testsuites_dict, platforms_list):
256356
""" Testing add_instances() function of TestPlan class in Twister
257357
Test 1: instances dictionary keys have expected values (Platform Name + Testcase Name)

0 commit comments

Comments
 (0)