blob: 9ba107580f7c3f0f61f9a43aba7e423e4854ad02 [file]
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import os
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest as pytest
from ntfc.pytest.configure import PytestConfigPlugin
def test_test_pytestconfigureplugin_init(config_dummy):
_ = PytestConfigPlugin(config_dummy)
def test_pytest_configure_log_file_uses_result_dir(
config_dummy, monkeypatch, tmp_path
):
"""Test pytest_configure sets log_file inside result_dir when available."""
monkeypatch.setattr(pytest, "result_dir", str(tmp_path), raising=False)
plugin = PytestConfigPlugin(config_dummy)
mock_config = MagicMock()
mock_config.option = MagicMock()
plugin.pytest_configure(mock_config)
assert mock_config.option.log_file == os.path.join(
str(tmp_path), "pytest.debug.log"
)
def test_pytest_configure_no_log_file_without_result_dir(
config_dummy, monkeypatch
):
"""Test pytest_configure skips log_file when result_dir is not set."""
monkeypatch.setattr(pytest, "result_dir", "", raising=False)
plugin = PytestConfigPlugin(config_dummy)
mock_config = MagicMock()
mock_config.option = MagicMock()
plugin.pytest_configure(mock_config)
# log_file_level is only set alongside log_file; if result_dir is empty
# the whole block is skipped — attribute stays a MagicMock, not "DEBUG"
assert mock_config.option.log_file_level != "DEBUG"
def test_pytest_generate_tests_with_core_fixture(config_dummy, monkeypatch):
"""Test pytest_generate_tests with core fixture."""
# Mock metafunc
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
metafunc.definition.iter_markers = MagicMock(return_value=[])
metafunc.definition.own_markers = []
plugin = PytestConfigPlugin(config_dummy)
plugin.pytest_generate_tests(metafunc)
# Should parametrize with default ['main'] when no --run_in_cores
metafunc.parametrize.assert_called_once_with("core", ["main"])
def test_pytest_generate_tests_with_extra_opts(config_dummy, monkeypatch):
"""Test pytest_generate_tests with --run_in_cores marker."""
# Mock metafunc
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
metafunc.definition.iter_markers = MagicMock(return_value=[])
# Create marker with --run_in_cores
marker = MagicMock()
marker.name = "extra_opts"
marker.args = ["--run_in_cores=main,cpu1,cpu2"]
metafunc.definition.own_markers = [marker]
plugin = PytestConfigPlugin(config_dummy)
plugin.pytest_generate_tests(metafunc)
# Should parametrize with ['main', 'cpu1', 'cpu2']
metafunc.parametrize.assert_called_once_with(
"core", ["main", "cpu1", "cpu2"]
)
def test_pytest_generate_tests_markers_edge_cases(config_dummy, monkeypatch):
"""Test pytest_generate_tests with various marker edge cases."""
plugin = PytestConfigPlugin(config_dummy)
# Test with non-extra_opts marker (branch 127->131)
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
metafunc.definition.iter_markers = MagicMock(return_value=[])
marker = MagicMock()
marker.name = "some_other_marker"
metafunc.definition.own_markers = [marker]
plugin.pytest_generate_tests(metafunc)
metafunc.parametrize.assert_called_once_with("core", ["main"])
# Test with extra_opts but wrong arg (branch 128->127)
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
metafunc.definition.iter_markers = MagicMock(return_value=[])
marker = MagicMock()
marker.name = "extra_opts"
marker.args = ["--some_other_option=value"]
metafunc.definition.own_markers = [marker]
plugin.pytest_generate_tests(metafunc)
metafunc.parametrize.assert_called_once_with("core", ["main"])
# Test with multiple markers triggering break (branch 131->125)
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
metafunc.definition.iter_markers = MagicMock(return_value=[])
marker1 = MagicMock()
marker1.name = "some_other_marker"
marker2 = MagicMock()
marker2.name = "extra_opts"
marker2.args = ["--run_in_cores=cpu1"]
metafunc.definition.own_markers = [marker1, marker2]
plugin.pytest_generate_tests(metafunc)
metafunc.parametrize.assert_called_once_with("core", ["cpu1"])
def test_pytest_generate_tests_without_core_fixture(config_dummy, monkeypatch):
"""Test pytest_generate_tests without core fixture."""
metafunc = MagicMock()
metafunc.fixturenames = ["other_fixture"]
plugin = PytestConfigPlugin(config_dummy)
plugin.pytest_generate_tests(metafunc)
# Should not parametrize if core fixture not used
metafunc.parametrize.assert_not_called()
def test_pytest_generate_tests_with_parametrize_marker(
config_dummy, monkeypatch
):
"""Test pytest_generate_tests with parametrize marker returns early."""
metafunc = MagicMock()
metafunc.fixturenames = ["core"]
metafunc.definition = MagicMock()
# Mock iter_markers to return a parametrize marker
mock_param_marker = MagicMock()
mock_param_marker.name = "parametrize"
metafunc.definition.iter_markers = MagicMock(
return_value=[mock_param_marker]
)
plugin = PytestConfigPlugin(config_dummy)
plugin.pytest_generate_tests(metafunc)
# Should not parametrize if test already has @pytest.mark.parametrize
metafunc.parametrize.assert_not_called()
def test_switch_to_core_fixture_no_product_object(config_dummy, monkeypatch):
"""Test switch_to_core fixture skips when no product object."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
# Explicitly set product to None/Falsy value
mock_pytest.product = None
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "main"
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
with pytest.raises(
pytest.skip.Exception, match="Pytest does not have product object"
):
next(gen)
def test_switch_to_core_fixture_not_smp(config_dummy, monkeypatch):
"""Test switch_to_core fixture skips when not SMP."""
# Mock pytest but keep real skip function
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip # Use real skip to raise exception
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create non-SMP product
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = False
mock_product.conf = mock_conf
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
# Create mock request
request = MagicMock()
# Get the unwrapped function (bypass pytest.fixture decorator)
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
# Get the generator and advance it to trigger the skip
gen = unwrapped(plugin, request)
# Should skip when not SMP
with pytest.raises(
pytest.skip.Exception, match="Product is not in SMP mode"
):
next(gen)
def test_switch_to_core_fixture_no_core_param(config_dummy, monkeypatch):
"""Test switch_to_core fixture skips when no core param."""
# Mock pytest but keep real skip function
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip # Use real skip to raise exception
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
# Create mock request without param
request = MagicMock()
request.param = None
# Get the unwrapped function (bypass pytest.fixture decorator)
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
# Get the generator and advance it to trigger the skip
gen = unwrapped(plugin, request)
# Should skip when no core param
with pytest.raises(pytest.skip.Exception, match="No core specified"):
next(gen)
def test_switch_to_core_fixture_no_core_list(config_dummy, monkeypatch):
"""Test switch_to_core fixture skips when no valid core list."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product with empty cores list
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = []
mock_product.conf = mock_conf
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "main"
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
with pytest.raises(
pytest.skip.Exception, match="Current product has no valid core list"
):
next(gen)
def test_switch_to_core_fixture_core_not_in_list(config_dummy, monkeypatch):
"""Test switch_to_core fixture skips when core not in list."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "cpu99" # Core not in list
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
with pytest.raises(
pytest.skip.Exception,
match="Current product does not have core: 'cpu99'",
):
next(gen)
def test_switch_to_core_fixture_main_core(config_dummy, monkeypatch):
"""Test switch_to_core fixture when already on main core."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "main" # Already on main core
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
# Should not raise, just yield
result = next(gen)
assert result is None
# Clean up the generator
try:
next(gen)
except StopIteration:
pass
def test_switch_to_core_fixture_switch_success(config_dummy, monkeypatch):
"""Test switch_to_core fixture when core switch succeeds."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product with mock core
mock_core0 = MagicMock()
mock_core0.switch_core.return_value = 0 # Success
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_product.core = MagicMock(return_value=mock_core0)
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "cpu1"
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
# Should not raise, just yield after switching
result = next(gen)
assert result is None
# Verify switch was called
mock_core0.switch_core.assert_called_once_with("cpu1")
# Clean up the generator
try:
next(gen)
except StopIteration:
pass
def test_switch_to_core_fixture_switch_failure(config_dummy, monkeypatch):
"""Test switch_to_core fixture when core switch fails."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product with mock core
mock_core0 = MagicMock()
mock_core0.switch_core.return_value = -1 # Failure
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_product.core = MagicMock(return_value=mock_core0)
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "cpu1"
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
with pytest.raises(
pytest.skip.Exception, match="Failed to switch to core cpu1"
):
next(gen)
def test_switch_to_core_fixture_switch_back_failure(config_dummy, monkeypatch):
"""Test switch_to_core fixture when switching back to main core fails."""
import pytest as real_pytest
mock_pytest = MagicMock()
mock_pytest.skip = real_pytest.skip
monkeypatch.setattr("ntfc.pytest.configure.pytest", mock_pytest)
# Create SMP product with mock core
mock_core0 = MagicMock()
# First switch succeeds (to cpu1), switch back fails
mock_core0.switch_core.side_effect = [0, -1]
mock_product = MagicMock()
mock_conf = MagicMock()
mock_conf.is_smp = True
mock_product.cores = ["main", "cpu1"]
mock_product.conf = mock_conf
mock_product.core = MagicMock(return_value=mock_core0)
mock_pytest.product = mock_product
plugin = PytestConfigPlugin(config_dummy)
request = MagicMock()
request.param = "cpu1"
unwrapped = plugin.switch_to_core.__wrapped__ # type: ignore
gen = unwrapped(plugin, request)
# Should not raise, just yield after switching
result = next(gen)
assert result is None
# Verify first switch was called
mock_core0.switch_core.assert_called_with("cpu1")
# Trigger cleanup by advancing generator (switch back fails,
# but doesn't raise)
try:
next(gen)
except StopIteration:
pass
# Verify switch back was attempted (second call)
assert mock_core0.switch_core.call_count == 2
def _run_makereport_hook(plugin, item, call, report):
outcome = MagicMock()
outcome.get_result.return_value = report
gen = plugin.pytest_runtest_makereport(item, call)
next(gen)
with pytest.raises(StopIteration):
gen.send(outcome)
def test_failure_reason_from_product_variants(config_dummy, monkeypatch):
plugin = PytestConfigPlugin(config_dummy)
cases = [
(
SimpleNamespace(
crash=True, busyloop=False, flood=False, notalive=False
),
(True, "crash", '"Device crashed"'),
),
(
SimpleNamespace(
crash=False, busyloop=True, flood=False, notalive=False
),
(True, "busy_loop", '"Device busy_loop"'),
),
(
SimpleNamespace(
crash=False, busyloop=False, flood=True, notalive=False
),
(True, "flood", '"Device flood"'),
),
(
SimpleNamespace(
crash=False, busyloop=False, flood=False, notalive=True
),
(True, "not_alive", '"Device not alive"'),
),
(
SimpleNamespace(
crash=False, busyloop=False, flood=False, notalive=False
),
(False, "", ""),
),
]
for product, expected in cases:
fake_pytest = SimpleNamespace(product=product)
monkeypatch.setattr("ntfc.pytest.configure.pytest", fake_pytest)
assert plugin._failure_reason_from_product() == expected
def test_is_crash_check_phase(config_dummy):
plugin = PytestConfigPlugin(config_dummy)
item = SimpleNamespace()
assert plugin._is_crash_check_phase(item, SimpleNamespace(when="setup"))
assert plugin._is_crash_check_phase(item, SimpleNamespace(when="call"))
assert plugin._is_crash_check_phase(item, SimpleNamespace(when="teardown"))
item2 = SimpleNamespace(_setup_call_failed=True)
assert not plugin._is_crash_check_phase(
item2, SimpleNamespace(when="teardown")
)
def test_debug_wait_seconds_variants(config_dummy, monkeypatch):
plugin = PytestConfigPlugin(config_dummy)
fake_pytest = SimpleNamespace(debug_time=7)
monkeypatch.setattr("ntfc.pytest.configure.pytest", fake_pytest)
assert plugin._debug_wait_seconds("failed", True) == 7
assert plugin._debug_wait_seconds("failed", False) == 0
assert plugin._debug_wait_seconds("passed", True) == 0
fake_pytest2 = SimpleNamespace()
monkeypatch.setattr("ntfc.pytest.configure.pytest", fake_pytest2)
assert plugin._debug_wait_seconds("failed", True) == 1800
def test_pytest_runtest_makereport_crash_path(config_dummy, monkeypatch):
product = SimpleNamespace(
crash=True, busyloop=False, flood=False, notalive=False
)
fake_pytest = SimpleNamespace(product=product, result_dir="")
monkeypatch.setattr("ntfc.pytest.configure.pytest", fake_pytest)
plugin = PytestConfigPlugin(config_dummy)
plugin._generate_coredump_file = MagicMock()
plugin._device_reboot = MagicMock()
item = SimpleNamespace()
call = SimpleNamespace(when="call")
report = SimpleNamespace(outcome="passed", longrepr="")
_run_makereport_hook(plugin, item, call, report)
assert report.outcome == "failed"
assert report.longrepr == '"Device crashed" detected, during: call'
assert getattr(item, "_setup_call_failed", False) is True
plugin._generate_coredump_file.assert_called_once_with("crash")
plugin._device_reboot.assert_called_once()
def test_pytest_runtest_makereport_crash_teardown_path(
config_dummy, monkeypatch
):
product = SimpleNamespace(
crash=True, busyloop=False, flood=False, notalive=False
)
fake_pytest = SimpleNamespace(product=product, result_dir="")
monkeypatch.setattr("ntfc.pytest.configure.pytest", fake_pytest)
plugin = PytestConfigPlugin(config_dummy)
plugin._generate_coredump_file = MagicMock()
plugin._device_reboot = MagicMock()
item = SimpleNamespace()
call = SimpleNamespace(when="teardown")
report = SimpleNamespace(outcome="passed", longrepr="")
_run_makereport_hook(plugin, item, call, report)
assert report.outcome == "failed"
assert not hasattr(item, "_setup_call_failed")
plugin._generate_coredump_file.assert_called_once_with("crash")
plugin._device_reboot.assert_called_once()