Skip to content

Commit 95bc519

Browse files
augusdnsreejithsreekumaran
authored andcommitted
Relocate log_custom_msg to assure test results to be logged (sonic-net#15007)
Instead of fixture, log_custom_msg is now called in one of hook (pytest_runtest_makereport). This is to prevent unexpected sequence of fixture teardown observed in nightly. Calling log_custom_msg from pytest_runtest_makereport will be one of the very last hooks to be called for each test. Description of PR Summary: Instead of fixture, log_custom_msg is now called in one of hook (pytest_runtest_makereport). This is to prevent unexpected sequence of fixture teardown observed in nightly. Calling log_custom_msg from pytest_runtest_makereport will be one of the very last hooks to be called for each test. Type of change Bug fix Testbed and Framework(new/improvement) Test case(new/improvement) Back port request 202012 202205 202305 202311 202405 Approach What is the motivation for this PR? log_custom_msg being called before post_sanity_check How did you do it? instead of fixture log_custom_msg, relocated the call to pytest_runtest_makereport How did you verify/test it? elastic-test using testplan co-authorized by: jianquanye@microsoft.com
1 parent a7db7ac commit 95bc519

4 files changed

Lines changed: 102 additions & 64 deletions

File tree

tests/common/helpers/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
ASIC_PARAM_TYPE_FRONTEND = 'frontend_asics'
77
ASICS_PRESENT = 'asics_present'
88
RANDOM_SEED = 'random_seed'
9+
CUSTOM_MSG_PREFIX = "sonic_custom_msg"
10+
DUT_CHECK_NAMESPACE = "dut_check_result"
911

1012
# Describe upstream neighbor of dut in different topos
1113
UPSTREAM_NEIGHBOR_MAP = {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
A helper module for log custom msg to be collected by kusto query.
3+
"""
4+
from tests.common.helpers.constants import (
5+
CUSTOM_MSG_PREFIX
6+
)
7+
8+
9+
def add_custom_msg(request, key, val):
10+
"""
11+
Add a custom message to the cache with a specified prefix.
12+
13+
At the end of each test, cached custom messages will be logged and collected
14+
by Kusto for debugging purposes.
15+
16+
Args:
17+
request: The pytest request object.
18+
key (str): The key for the custom message. Use '.' to separate different
19+
levels of keys (e.g., "foo.bar.baz" will be stored as
20+
{ "foo": { "bar": { "baz": val } } }).
21+
val: The value to be stored in the cache under the specified key.
22+
"""
23+
request.config.cache.set(f"{CUSTOM_MSG_PREFIX}.{key}", val)

tests/common/plugins/sanity_check/__init__.py

Lines changed: 23 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
from tests.common.plugins.sanity_check.recover import recover, recover_chassis
1515
from tests.common.plugins.sanity_check.constants import STAGE_PRE_TEST, STAGE_POST_TEST
1616
from tests.common.helpers.assertions import pytest_assert as pt_assert
17+
from tests.common.helpers.custom_msg_utils import add_custom_msg
18+
from tests.common.helpers.constants import (
19+
DUT_CHECK_NAMESPACE
20+
)
1721

1822
logger = logging.getLogger(__name__)
1923

2024
SUPPORTED_CHECKS = checks.CHECK_ITEMS
21-
DUT_CHEK_LIST = ['core_dump_check_pass', 'config_db_check_pass']
22-
CACHE_LIST = ['core_dump_check_pass', 'config_db_check_pass',
23-
'pre_sanity_recovered', 'post_sanity_recovered']
25+
CUSTOM_MSG_PREFIX = "sonic_custom_msg"
2426

2527

2628
def pytest_sessionfinish(session, exitstatus):
@@ -32,8 +34,6 @@ def pytest_sessionfinish(session, exitstatus):
3234
session.config.cache.set("pre_sanity_check_failed", None)
3335
if post_sanity_failed:
3436
session.config.cache.set("post_sanity_check_failed", None)
35-
for key in CACHE_LIST:
36-
session.config.cache.set(key, None)
3737

3838
if pre_sanity_failed and not post_sanity_failed:
3939
session.exitstatus = constants.PRE_SANITY_CHECK_FAILED_RC
@@ -125,47 +125,6 @@ def do_checks(request, check_items, *args, **kwargs):
125125
return check_results
126126

127127

128-
@pytest.fixture(scope="module", autouse=True)
129-
def log_custom_msg(request):
130-
yield
131-
module_name = request.node.name
132-
items = request.session.items
133-
for item in items:
134-
if item.module.__name__ + ".py" == module_name.split("/")[-1]:
135-
customMsgDict = {}
136-
dutChekResults = {}
137-
for key in DUT_CHEK_LIST:
138-
if request.config.cache.get(key, None) is False:
139-
dutChekResults[key] = False
140-
if dutChekResults:
141-
customMsgDict['DutChekResult'] = dutChekResults
142-
143-
# Check pre_sanity_checks results
144-
preSanityCheckResults = {}
145-
if request.config.cache.get("pre_sanity_check_failed", None):
146-
preSanityCheckResults['pre_sanity_check_failed'] = True
147-
# pre_sanity_recovered should be None in healthy case, record either True/False
148-
if request.config.cache.get("pre_sanity_recovered", None) is not None:
149-
preSanityCheckResults['pre_sanity_recovered'] = request.config.cache.get("pre_sanity_recovered", None)
150-
if preSanityCheckResults:
151-
customMsgDict['PreSanityCheckResults'] = preSanityCheckResults
152-
153-
# Check post_sanity_checks results
154-
postSanityCheckResults = {}
155-
if request.config.cache.get("post_sanity_check_failed", None):
156-
postSanityCheckResults['post_sanity_check_failed'] = True
157-
# post_sanity_recovered should be None in healthy case, record either True/False
158-
if request.config.cache.get("post_sanity_recovered", None) is not None:
159-
preSanityCheckResults['post_sanity_recovered'] = request.config.cache.get("post_sanity_recovered", None)
160-
if postSanityCheckResults:
161-
customMsgDict['PostSanityCheckResults'] = postSanityCheckResults
162-
163-
# if we have any custom message to log, append it to user_properties
164-
if customMsgDict:
165-
logger.debug("customMsgDict: {}".format(customMsgDict))
166-
item.user_properties.append(('CustomMsg', json.dumps(customMsgDict)))
167-
168-
169128
@pytest.fixture(scope="module")
170129
def prepare_parallel_run(request, parallel_run_context):
171130
is_par_run, target_hostname, is_par_leader, par_followers, par_state_file = parallel_run_context
@@ -330,6 +289,7 @@ def sanity_check_full(prepare_parallel_run, localhost, duthosts, request, fanout
330289
if failed_results:
331290
if not allow_recover:
332291
request.config.cache.set("pre_sanity_check_failed", True)
292+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.pre_sanity_check_failed", True)
333293
pt_assert(False, "!!!!!!!!!!!!!!!!Pre-test sanity check failed: !!!!!!!!!!!!!!!!\n{}"
334294
.format(json.dumps(failed_results, indent=4, default=fallback_serializer)))
335295
else:
@@ -351,15 +311,16 @@ def sanity_check_full(prepare_parallel_run, localhost, duthosts, request, fanout
351311
logger.debug("Post-test sanity check results:\n%s" %
352312
json.dumps(post_check_results, indent=4, default=fallback_serializer))
353313

354-
post_failed_results = [result for result in post_check_results if result['failed']]
355-
if post_failed_results:
356-
if not allow_recover:
357-
request.config.cache.set("post_sanity_check_failed", True)
358-
pt_assert(False, "!!!!!!!!!!!!!!!! Post-test sanity check failed: !!!!!!!!!!!!!!!!\n{}"
359-
.format(json.dumps(post_failed_results, indent=4, default=fallback_serializer)))
360-
else:
361-
recover_on_sanity_check_failure(duthosts, post_failed_results, fanouthosts, localhost, nbrhosts,
362-
post_check_items, recover_method, request, tbinfo, STAGE_POST_TEST)
314+
post_failed_results = [result for result in post_check_results if result['failed']]
315+
if post_failed_results:
316+
if not allow_recover:
317+
request.config.cache.set("post_sanity_check_failed", True)
318+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.post_sanity_check_failed", True)
319+
pt_assert(False, "!!!!!!!!!!!!!!!! Post-test sanity check failed: !!!!!!!!!!!!!!!!\n{}"
320+
.format(json.dumps(post_failed_results, indent=4, default=fallback_serializer)))
321+
else:
322+
recover_on_sanity_check_failure(duthosts, post_failed_results, fanouthosts, localhost, nbrhosts,
323+
post_check_items, recover_method, request, tbinfo, STAGE_POST_TEST)
363324

364325
logger.info("Done post-test sanity check")
365326
else:
@@ -401,7 +362,8 @@ def recover_on_sanity_check_failure(duthosts, failed_results, fanouthosts, local
401362

402363
except BaseException as e:
403364
request.config.cache.set(cache_key, True)
404-
request.config.cache.set(recovery_cache_key, False)
365+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.{cache_key}", True)
366+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.{recovery_cache_key}", False)
405367

406368
logger.error(f"Recovery of sanity check failed with exception: {repr(e)}")
407369
pt_assert(
@@ -416,17 +378,18 @@ def recover_on_sanity_check_failure(duthosts, failed_results, fanouthosts, local
416378
new_failed_results = [result for result in new_check_results if result['failed']]
417379
if new_failed_results:
418380
request.config.cache.set(cache_key, True)
419-
request.config.cache.set(recovery_cache_key, False)
381+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.{cache_key}", True)
382+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.{recovery_cache_key}", False)
420383
pt_assert(False,
421384
f"!!!!!!!!!!!!!!!! {sanity_check_stage} sanity check after recovery failed: !!!!!!!!!!!!!!!!\n"
422385
f"{json.dumps(new_failed_results, indent=4, default=fallback_serializer)}")
423386
# Record recovery success
424-
request.config.cache.set(recovery_cache_key, True)
387+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.{recovery_cache_key}", True)
425388

426389

427-
# make sure teardown of log_custom_msg happens after sanity_check
428390
@pytest.fixture(scope="module", autouse=True)
429-
def sanity_check(request, parallel_run_context, log_custom_msg):
391+
def sanity_check(request, parallel_run_context):
392+
430393
is_par_run, target_hostname, is_par_leader, par_followers, par_state_file = parallel_run_context
431394
initial_check_state = InitialCheckState(par_followers, par_state_file) if is_par_run else None
432395
if is_par_run:

tests/conftest.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838
from tests.common.dualtor.dual_tor_utils import disable_timed_oscillation_active_standby# noqa F401
3939

4040
from tests.common.helpers.constants import (
41-
ASIC_PARAM_TYPE_ALL, ASIC_PARAM_TYPE_FRONTEND, DEFAULT_ASIC_ID, ASICS_PRESENT
41+
ASIC_PARAM_TYPE_ALL, ASIC_PARAM_TYPE_FRONTEND, DEFAULT_ASIC_ID, ASICS_PRESENT, DUT_CHECK_NAMESPACE
4242
)
43+
from tests.common.helpers.custom_msg_utils import add_custom_msg
4344
from tests.common.helpers.dut_ports import encode_dut_port_name
4445
from tests.common.helpers.dut_utils import encode_dut_and_container_name
4546
from tests.common.helpers.parallel_utils import InitialCheckState, InitialCheckStatus
@@ -76,6 +77,7 @@
7677
cache = FactsCache()
7778

7879
DUTHOSTS_FIXTURE_FAILED_RC = 15
80+
CUSTOM_MSG_PREFIX = "sonic_custom_msg"
7981

8082
pytest_plugins = ('tests.common.plugins.ptfadapter',
8183
'tests.common.plugins.ansible_fixtures',
@@ -388,6 +390,16 @@ def get_specified_duts(request):
388390
return duts
389391

390392

393+
def pytest_sessionstart(session):
394+
# reset all the sonic_custom_msg keys from cache
395+
# reset here because this fixture will always be very first fixture to be called
396+
cache_dir = session.config.cache._cachedir
397+
keys = [p.name for p in cache_dir.glob('**/*') if p.is_file() and p.name.startswith(CUSTOM_MSG_PREFIX)]
398+
for key in keys:
399+
logger.debug("reset existing key: {}".format(key))
400+
session.config.cache.set(key, None)
401+
402+
391403
def pytest_sessionfinish(session, exitstatus):
392404
if session.config.cache.get("duthosts_fixture_failed", None):
393405
session.config.cache.set("duthosts_fixture_failed", None)
@@ -914,12 +926,50 @@ def creds_all_duts(duthosts):
914926
return creds_all_duts
915927

916928

929+
def update_custom_msg(custom_msg, key, value):
930+
if custom_msg is None:
931+
custom_msg = {}
932+
chunks = key.split('.')
933+
if chunks[0] == CUSTOM_MSG_PREFIX:
934+
chunks = chunks[1:]
935+
if len(chunks) == 1:
936+
custom_msg.update({chunks[0]: value})
937+
return custom_msg
938+
if chunks[0] not in custom_msg:
939+
custom_msg[chunks[0]] = {}
940+
custom_msg[chunks[0]] = update_custom_msg(custom_msg[chunks[0]], '.'.join(chunks[1:]), value)
941+
return custom_msg
942+
943+
944+
def log_custom_msg(item):
945+
# temp log output to track module name
946+
logger.debug("[log_custom_msg] item: {}".format(item))
947+
948+
cache_dir = item.session.config.cache._cachedir
949+
keys = [p.name for p in cache_dir.glob('**/*') if p.is_file() and p.name.startswith(CUSTOM_MSG_PREFIX)]
950+
951+
custom_msg = {}
952+
for key in keys:
953+
value = item.session.config.cache.get(key, None)
954+
if value is not None:
955+
custom_msg = update_custom_msg(custom_msg, key, value)
956+
957+
if custom_msg:
958+
logger.debug("append custom_msg: {}".format(custom_msg))
959+
item.user_properties.append(('CustomMsg', json.dumps(custom_msg)))
960+
961+
962+
# This function is a pytest hook implementation that is called to create a test report.
963+
# By placing the call to log_custom_msg in the 'teardown' phase, we ensure that it is executed
964+
# at the end of each test, after all other fixture teardowns. This guarantees that any custom
965+
# messages are logged at the latest possible stage in the test lifecycle.
917966
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
918967
def pytest_runtest_makereport(item, call):
919968

920969
if call.when == 'setup':
921970
item.user_properties.append(('start', str(datetime.fromtimestamp(call.start))))
922971
elif call.when == 'teardown':
972+
log_custom_msg(item)
923973
item.user_properties.append(('end', str(datetime.fromtimestamp(call.stop))))
924974

925975
# Filter out unnecessary logs captured on "stdout" and "stderr"
@@ -2489,10 +2539,10 @@ def _remove_entry(table_name, key_name, config):
24892539
logger.debug('Results of dut reload: {}'.format(json.dumps(dict(results))))
24902540
else:
24912541
logger.info("Core dump and config check passed for {}".format(module_name))
2492-
24932542
if check_result:
2494-
request.config.cache.set("core_dump_check_pass", core_dump_check_pass)
2495-
request.config.cache.set("config_db_check_pass", config_db_check_pass)
2543+
logger.debug("core_dump_and_config_check failed, check_result: {}".format(json.dumps(check_result)))
2544+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.core_dump_check_pass", core_dump_check_pass)
2545+
add_custom_msg(request, f"{DUT_CHECK_NAMESPACE}.config_db_check_pass", config_db_check_pass)
24962546

24972547

24982548
@pytest.fixture(scope="function")

0 commit comments

Comments
 (0)