From e165813341d457270feb80e7e65a0ff557b385e5 Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Wed, 4 Feb 2026 15:23:19 -0600 Subject: [PATCH 1/4] pytest infra warnings --- dpnp/tests/conftest.py | 127 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/dpnp/tests/conftest.py b/dpnp/tests/conftest.py index bd6c71f9a92b..12ddea967806 100644 --- a/dpnp/tests/conftest.py +++ b/dpnp/tests/conftest.py @@ -28,12 +28,15 @@ import os import sys +import json import warnings import dpctl import numpy import pytest +from collections import Counter + from . import config as dtype_config if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0b1": @@ -78,6 +81,24 @@ def normalize_test_name(nodeid): return normalized_nodeid +DPNP_INFRA_WARNINGS_ENABLE = os.getenv("DPNP_INFRA_WARNINGS_ENABLE", "0") == "1" +DPNP_INFRA_WARNINGS_DIRECTORY = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) + +def _origin_from_filename(filename: str) -> str: + file = (filename or "").replace("\\", "/") + if "/dpnp/" in file or file.startswith("dpnp/"): + return "dpnp" + elif "/numpy/" in file or file.startswith("numpy/"): + return "numpy" + elif "/dpctl/" in file or file.startswith("dpctl/"): + return "dpctl" + else: + return "third_party" + +_warn_counts = Counter() +_warn_examples = {} +_warn_totals = Counter() +_warn_env = {} def pytest_configure(config): # By default, tests marked as slow will be deselected. @@ -114,6 +135,34 @@ def pytest_configure(config): "ignore:invalid value encountered in arccosh:RuntimeWarning", ) + if DPNP_INFRA_WARNINGS_ENABLE: + try: + numpy_version = numpy.__version__ + numpy_path = getattr(numpy, "__file__", "unknown") + dpnp_version = dpnp.__version__ + dpnp_path = getattr(dpnp, "__file__", "unknown") + dpctl_version = dpctl.__version__ + dpctl_path = getattr(dpctl, "__file__", "unknown") + except Exception: + numpy_version = "unknown" + dpnp_version = "unknown" + numpy_path = "unknown" + dpnp_path = "unknown" + dpctl_version = "unknown" + dpctl_path = "unknown" + + _warn_env.update({ + "numpy_version": numpy_version, + "numpy_path": numpy_path, + "dpnp_version": dpnp_version, + "dpnp_path": dpnp_path, + "dpctl_version": dpctl_version, + "dpctl_path": dpctl_path, + "job": os.getenv("JOB_NAME", "unknown"), + "build_number": os.getenv("BUILD_NUMBER", "unknown"), + "git_sha": os.getenv("GIT_COMMIT", "unknown"), + }) + def pytest_collection_modifyitems(config, items): test_path = os.path.split(__file__)[0] @@ -236,3 +285,81 @@ def suppress_divide_invalid_numpy_warnings( suppress_divide_numpy_warnings, suppress_invalid_numpy_warnings ): yield + + +def pytest_warning_recorded(warning_message, when, nodeid, location): + if not DPNP_INFRA_WARNINGS_ENABLE: + return + + category = getattr( + getattr(warning_message, "category", None), + "__name__", + str(getattr(warning_message, "category", "Warning")), + ) + msg = str(getattr(warning_message, "message", warning_message)) + + filename = getattr(warning_message, "filename", None) or ( + location[0] if location and len(location) > 0 else None + ) + lineno = getattr(warning_message, "lineno", None) or ( + location[1] if location and len(location) > 1 else None + ) + func = location[2] if location and len(location) > 2 else None + + origin = _origin_from_filename(filename or "") + key = f"{category}||{origin}||{msg}" + _warn_counts[key] += 1 + _warn_totals[f"category::{category}"] += 1 + _warn_totals[f"origin::{origin}"] += 1 + _warn_totals[f"phase::{when}"] += 1 + + if key not in _warn_examples: + _warn_examples[key] = { + "category": category, + "origin": origin, + "when": when, + "nodeid": nodeid, + "filename": filename, + "lineno": lineno, + "function": func, + "message": msg, + } + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + if not DPNP_INFRA_WARNINGS_ENABLE: + return + + summary = { + "schema_version": "1.0", + "exit_status": exitstatus, + "environment": dict(_warn_env), + "total_warning_events": sum(_warn_counts.values()), + "unique_warning_types": len(_warn_counts), + "totals": dict(_warn_totals), + "top_unique_warnings": [ + dict(_warn_examples[k], count=c) + for k, c in _warn_counts.most_common(50) + if k in _warn_examples + ], + } + + if DPNP_INFRA_WARNINGS_DIRECTORY: + os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) + output_file = os.path.join( + DPNP_INFRA_WARNINGS_DIRECTORY, "dpnp_infra_warnings_summary.json" + ) + try: + with open(output_file, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=2, sort_keys=True) + terminalreporter.write_line( + f"DPNP infrastructure warnings summary written to: {output_file}" + ) + except Exception as e: + terminalreporter.write_line( + f"Failed to write DPNP infrastructure warnings summary to: {output_file}. Error: {e}" + ) + + terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_BEGIN") + terminalreporter.write_line(json.dumps(summary, sort_keys=True)) + terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_END") From ad1541d5dc1b96dae2b948ebcd9d7b1952695337 Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Wed, 4 Feb 2026 17:59:55 -0600 Subject: [PATCH 2/4] events artifact --- dpnp/tests/conftest.py | 87 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/dpnp/tests/conftest.py b/dpnp/tests/conftest.py index 12ddea967806..238e8e30a7ea 100644 --- a/dpnp/tests/conftest.py +++ b/dpnp/tests/conftest.py @@ -96,9 +96,16 @@ def _origin_from_filename(filename: str) -> str: return "third_party" _warn_counts = Counter() -_warn_examples = {} +_warn_events = {} _warn_totals = Counter() _warn_env = {} +_warn_events_fp = None +_warn_events_file = None +_warn_print_events = os.getenv("DPNP_INFRA_WARNINGS_PRINT", "0") == "1" + + +def _json_dumps_one_line(obj) -> str: + return json.dumps(obj, separators=(",", ":")) def pytest_configure(config): # By default, tests marked as slow will be deselected. @@ -151,6 +158,28 @@ def pytest_configure(config): dpctl_version = "unknown" dpctl_path = "unknown" + global _warn_events_fp + global _warn_events_file + if DPNP_INFRA_WARNINGS_DIRECTORY: + os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) + # Optional override for file name. + events_file_override = os.getenv("DPNP_INFRA_WARNINGS_EVENTS_FILE") + if events_file_override: + _warn_events_file = events_file_override + else: + _warn_events_file = os.path.join( + DPNP_INFRA_WARNINGS_DIRECTORY, + "dpnp_infra_warnings_events.jsonl", + ) + # Line-buffered for streaming CI artifacts without holding everything in memory. + _warn_events_fp = open( + _warn_events_file, + "w", + encoding="utf-8", + buffering=1, + newline="\n", + ) + _warn_env.update({ "numpy_version": numpy_version, "numpy_path": numpy_path, @@ -161,6 +190,7 @@ def pytest_configure(config): "job": os.getenv("JOB_NAME", "unknown"), "build_number": os.getenv("BUILD_NUMBER", "unknown"), "git_sha": os.getenv("GIT_COMMIT", "unknown"), + "events_file": _warn_events_file, }) @@ -313,8 +343,8 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): _warn_totals[f"origin::{origin}"] += 1 _warn_totals[f"phase::{when}"] += 1 - if key not in _warn_examples: - _warn_examples[key] = { + if key not in _warn_events: + _warn_events[key] = { "category": category, "origin": origin, "when": when, @@ -325,6 +355,33 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): "message": msg, } + event = { + "when": when, + "nodeid": nodeid, + "category": category, + "origin": origin, + "message": msg, + "filename": filename, + "lineno": lineno, + "function": func, + } + + # Write every warning event to artifact (JSONL). + if _warn_events_fp is not None: + try: + _warn_events_fp.write(_json_dumps_one_line(event) + "\n") + except Exception: + # Never break the test run due to artifact IO. + pass + + # Optionally print every warning event to console (one-line JSON). + if _warn_print_events: + try: + sys.stderr.write("DPNP_WARNING_EVENT " + _json_dumps_one_line(event) + "\n") + sys.stderr.flush() + except Exception: + pass + def pytest_terminal_summary(terminalreporter, exitstatus, config): if not DPNP_INFRA_WARNINGS_ENABLE: @@ -338,9 +395,9 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): "unique_warning_types": len(_warn_counts), "totals": dict(_warn_totals), "top_unique_warnings": [ - dict(_warn_examples[k], count=c) + dict(_warn_events[k], count=c) for k, c in _warn_counts.most_common(50) - if k in _warn_examples + if k in _warn_events ], } @@ -360,6 +417,24 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): f"Failed to write DPNP infrastructure warnings summary to: {output_file}. Error: {e}" ) + global _warn_events_fp + if _warn_events_fp is not None: + try: + _warn_events_fp.close() + except Exception: + pass + _warn_events_fp = None + terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_BEGIN") - terminalreporter.write_line(json.dumps(summary, sort_keys=True)) + terminalreporter.write_line(_json_dumps_one_line(summary)) terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_END") + + +def pytest_unconfigure(config): + global _warn_events_fp + if _warn_events_fp is not None: + try: + _warn_events_fp.close() + except Exception: + pass + _warn_events_fp = None From 9baa34bf41a38c503fec6458eb8f286866d7a22a Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Wed, 4 Feb 2026 20:02:53 -0600 Subject: [PATCH 3/4] cleanup --- dpnp/tests/conftest.py | 65 ++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/dpnp/tests/conftest.py b/dpnp/tests/conftest.py index 238e8e30a7ea..055eaafa5b51 100644 --- a/dpnp/tests/conftest.py +++ b/dpnp/tests/conftest.py @@ -83,6 +83,15 @@ def normalize_test_name(nodeid): DPNP_INFRA_WARNINGS_ENABLE = os.getenv("DPNP_INFRA_WARNINGS_ENABLE", "0") == "1" DPNP_INFRA_WARNINGS_DIRECTORY = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) +DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT = "dpnp_infra_warnings_events.jsonl" +DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT = "dpnp_infra_warnings_summary.json" +_warn_counts = Counter() +_warn_events = {} +_warn_totals = Counter() +_warn_env = {} +_warn_events_fp = None +_WARN_EVENTS_FILE = None + def _origin_from_filename(filename: str) -> str: file = (filename or "").replace("\\", "/") @@ -95,18 +104,11 @@ def _origin_from_filename(filename: str) -> str: else: return "third_party" -_warn_counts = Counter() -_warn_events = {} -_warn_totals = Counter() -_warn_env = {} -_warn_events_fp = None -_warn_events_file = None -_warn_print_events = os.getenv("DPNP_INFRA_WARNINGS_PRINT", "0") == "1" - def _json_dumps_one_line(obj) -> str: return json.dumps(obj, separators=(",", ":")) + def pytest_configure(config): # By default, tests marked as slow will be deselected. # To run all tests, use -m "slow or not slow". @@ -143,6 +145,7 @@ def pytest_configure(config): ) if DPNP_INFRA_WARNINGS_ENABLE: + # Capture packages information try: numpy_version = numpy.__version__ numpy_path = getattr(numpy, "__file__", "unknown") @@ -159,21 +162,15 @@ def pytest_configure(config): dpctl_path = "unknown" global _warn_events_fp - global _warn_events_file + global _WARN_EVENTS_FILE if DPNP_INFRA_WARNINGS_DIRECTORY: os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) - # Optional override for file name. - events_file_override = os.getenv("DPNP_INFRA_WARNINGS_EVENTS_FILE") - if events_file_override: - _warn_events_file = events_file_override - else: - _warn_events_file = os.path.join( - DPNP_INFRA_WARNINGS_DIRECTORY, - "dpnp_infra_warnings_events.jsonl", - ) - # Line-buffered for streaming CI artifacts without holding everything in memory. + _WARN_EVENTS_FILE = os.path.join( + DPNP_INFRA_WARNINGS_DIRECTORY, + DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT, + ) _warn_events_fp = open( - _warn_events_file, + _WARN_EVENTS_FILE, "w", encoding="utf-8", buffering=1, @@ -190,7 +187,7 @@ def pytest_configure(config): "job": os.getenv("JOB_NAME", "unknown"), "build_number": os.getenv("BUILD_NUMBER", "unknown"), "git_sha": os.getenv("GIT_COMMIT", "unknown"), - "events_file": _warn_events_file, + "events_file": _WARN_EVENTS_FILE, }) @@ -326,7 +323,7 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): "__name__", str(getattr(warning_message, "category", "Warning")), ) - msg = str(getattr(warning_message, "message", warning_message)) + message = str(getattr(warning_message, "message", warning_message)) filename = getattr(warning_message, "filename", None) or ( location[0] if location and len(location) > 0 else None @@ -337,7 +334,7 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): func = location[2] if location and len(location) > 2 else None origin = _origin_from_filename(filename or "") - key = f"{category}||{origin}||{msg}" + key = f"{category}||{origin}||{message}" _warn_counts[key] += 1 _warn_totals[f"category::{category}"] += 1 _warn_totals[f"origin::{origin}"] += 1 @@ -352,7 +349,7 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): "filename": filename, "lineno": lineno, "function": func, - "message": msg, + "message": message, } event = { @@ -360,25 +357,15 @@ def pytest_warning_recorded(warning_message, when, nodeid, location): "nodeid": nodeid, "category": category, "origin": origin, - "message": msg, + "message": message, "filename": filename, "lineno": lineno, "function": func, } - # Write every warning event to artifact (JSONL). if _warn_events_fp is not None: try: _warn_events_fp.write(_json_dumps_one_line(event) + "\n") - except Exception: - # Never break the test run due to artifact IO. - pass - - # Optionally print every warning event to console (one-line JSON). - if _warn_print_events: - try: - sys.stderr.write("DPNP_WARNING_EVENT " + _json_dumps_one_line(event) + "\n") - sys.stderr.flush() except Exception: pass @@ -404,17 +391,19 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): if DPNP_INFRA_WARNINGS_DIRECTORY: os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) output_file = os.path.join( - DPNP_INFRA_WARNINGS_DIRECTORY, "dpnp_infra_warnings_summary.json" + DPNP_INFRA_WARNINGS_DIRECTORY, DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT ) try: with open(output_file, "w", encoding="utf-8") as f: json.dump(summary, f, indent=2, sort_keys=True) terminalreporter.write_line( - f"DPNP infrastructure warnings summary written to: {output_file}" + f"DPNP infrastructure warnings summary written " \ + f"to: {output_file}" ) except Exception as e: terminalreporter.write_line( - f"Failed to write DPNP infrastructure warnings summary to: {output_file}. Error: {e}" + f"Failed to write DPNP infrastructure warnings" \ + f" summary to: {output_file}. Error: {e}" ) global _warn_events_fp From 2ec7e2f03e0b0b265f1bb785da7ecae510b471da Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Thu, 5 Feb 2026 09:57:49 -0600 Subject: [PATCH 4/4] refactor warning plugin --- dpnp/tests/conftest.py | 192 +----------------------- dpnp/tests/infra_warning_utils.py | 239 ++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 190 deletions(-) create mode 100644 dpnp/tests/infra_warning_utils.py diff --git a/dpnp/tests/conftest.py b/dpnp/tests/conftest.py index 055eaafa5b51..5d766566bca5 100644 --- a/dpnp/tests/conftest.py +++ b/dpnp/tests/conftest.py @@ -28,15 +28,12 @@ import os import sys -import json import warnings import dpctl import numpy import pytest -from collections import Counter - from . import config as dtype_config if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0b1": @@ -47,6 +44,7 @@ import dpnp from .helper import get_dev_id +from .infra_warning_utils import register_infra_warnings_plugin_if_enabled skip_mark = pytest.mark.skip(reason="Skipping test.") @@ -81,33 +79,6 @@ def normalize_test_name(nodeid): return normalized_nodeid -DPNP_INFRA_WARNINGS_ENABLE = os.getenv("DPNP_INFRA_WARNINGS_ENABLE", "0") == "1" -DPNP_INFRA_WARNINGS_DIRECTORY = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) -DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT = "dpnp_infra_warnings_events.jsonl" -DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT = "dpnp_infra_warnings_summary.json" -_warn_counts = Counter() -_warn_events = {} -_warn_totals = Counter() -_warn_env = {} -_warn_events_fp = None -_WARN_EVENTS_FILE = None - - -def _origin_from_filename(filename: str) -> str: - file = (filename or "").replace("\\", "/") - if "/dpnp/" in file or file.startswith("dpnp/"): - return "dpnp" - elif "/numpy/" in file or file.startswith("numpy/"): - return "numpy" - elif "/dpctl/" in file or file.startswith("dpctl/"): - return "dpctl" - else: - return "third_party" - - -def _json_dumps_one_line(obj) -> str: - return json.dumps(obj, separators=(",", ":")) - def pytest_configure(config): # By default, tests marked as slow will be deselected. @@ -144,51 +115,7 @@ def pytest_configure(config): "ignore:invalid value encountered in arccosh:RuntimeWarning", ) - if DPNP_INFRA_WARNINGS_ENABLE: - # Capture packages information - try: - numpy_version = numpy.__version__ - numpy_path = getattr(numpy, "__file__", "unknown") - dpnp_version = dpnp.__version__ - dpnp_path = getattr(dpnp, "__file__", "unknown") - dpctl_version = dpctl.__version__ - dpctl_path = getattr(dpctl, "__file__", "unknown") - except Exception: - numpy_version = "unknown" - dpnp_version = "unknown" - numpy_path = "unknown" - dpnp_path = "unknown" - dpctl_version = "unknown" - dpctl_path = "unknown" - - global _warn_events_fp - global _WARN_EVENTS_FILE - if DPNP_INFRA_WARNINGS_DIRECTORY: - os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) - _WARN_EVENTS_FILE = os.path.join( - DPNP_INFRA_WARNINGS_DIRECTORY, - DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT, - ) - _warn_events_fp = open( - _WARN_EVENTS_FILE, - "w", - encoding="utf-8", - buffering=1, - newline="\n", - ) - - _warn_env.update({ - "numpy_version": numpy_version, - "numpy_path": numpy_path, - "dpnp_version": dpnp_version, - "dpnp_path": dpnp_path, - "dpctl_version": dpctl_version, - "dpctl_path": dpctl_path, - "job": os.getenv("JOB_NAME", "unknown"), - "build_number": os.getenv("BUILD_NUMBER", "unknown"), - "git_sha": os.getenv("GIT_COMMIT", "unknown"), - "events_file": _WARN_EVENTS_FILE, - }) + register_infra_warnings_plugin_if_enabled(config) def pytest_collection_modifyitems(config, items): @@ -312,118 +239,3 @@ def suppress_divide_invalid_numpy_warnings( suppress_divide_numpy_warnings, suppress_invalid_numpy_warnings ): yield - - -def pytest_warning_recorded(warning_message, when, nodeid, location): - if not DPNP_INFRA_WARNINGS_ENABLE: - return - - category = getattr( - getattr(warning_message, "category", None), - "__name__", - str(getattr(warning_message, "category", "Warning")), - ) - message = str(getattr(warning_message, "message", warning_message)) - - filename = getattr(warning_message, "filename", None) or ( - location[0] if location and len(location) > 0 else None - ) - lineno = getattr(warning_message, "lineno", None) or ( - location[1] if location and len(location) > 1 else None - ) - func = location[2] if location and len(location) > 2 else None - - origin = _origin_from_filename(filename or "") - key = f"{category}||{origin}||{message}" - _warn_counts[key] += 1 - _warn_totals[f"category::{category}"] += 1 - _warn_totals[f"origin::{origin}"] += 1 - _warn_totals[f"phase::{when}"] += 1 - - if key not in _warn_events: - _warn_events[key] = { - "category": category, - "origin": origin, - "when": when, - "nodeid": nodeid, - "filename": filename, - "lineno": lineno, - "function": func, - "message": message, - } - - event = { - "when": when, - "nodeid": nodeid, - "category": category, - "origin": origin, - "message": message, - "filename": filename, - "lineno": lineno, - "function": func, - } - - if _warn_events_fp is not None: - try: - _warn_events_fp.write(_json_dumps_one_line(event) + "\n") - except Exception: - pass - - -def pytest_terminal_summary(terminalreporter, exitstatus, config): - if not DPNP_INFRA_WARNINGS_ENABLE: - return - - summary = { - "schema_version": "1.0", - "exit_status": exitstatus, - "environment": dict(_warn_env), - "total_warning_events": sum(_warn_counts.values()), - "unique_warning_types": len(_warn_counts), - "totals": dict(_warn_totals), - "top_unique_warnings": [ - dict(_warn_events[k], count=c) - for k, c in _warn_counts.most_common(50) - if k in _warn_events - ], - } - - if DPNP_INFRA_WARNINGS_DIRECTORY: - os.makedirs(DPNP_INFRA_WARNINGS_DIRECTORY, exist_ok=True) - output_file = os.path.join( - DPNP_INFRA_WARNINGS_DIRECTORY, DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT - ) - try: - with open(output_file, "w", encoding="utf-8") as f: - json.dump(summary, f, indent=2, sort_keys=True) - terminalreporter.write_line( - f"DPNP infrastructure warnings summary written " \ - f"to: {output_file}" - ) - except Exception as e: - terminalreporter.write_line( - f"Failed to write DPNP infrastructure warnings" \ - f" summary to: {output_file}. Error: {e}" - ) - - global _warn_events_fp - if _warn_events_fp is not None: - try: - _warn_events_fp.close() - except Exception: - pass - _warn_events_fp = None - - terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_BEGIN") - terminalreporter.write_line(_json_dumps_one_line(summary)) - terminalreporter.write_line("DPNP_WARNINGS_SUMMARY_END") - - -def pytest_unconfigure(config): - global _warn_events_fp - if _warn_events_fp is not None: - try: - _warn_events_fp.close() - except Exception: - pass - _warn_events_fp = None diff --git a/dpnp/tests/infra_warning_utils.py b/dpnp/tests/infra_warning_utils.py new file mode 100644 index 000000000000..5672cbce4370 --- /dev/null +++ b/dpnp/tests/infra_warning_utils.py @@ -0,0 +1,239 @@ +import json +import os +import sys + +from collections import Counter + +import dpctl +import numpy + +import dpnp + + +def _env_check(var_name: str, *, default: bool = False) -> bool: + value = os.getenv(var_name) + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "y", "on"} + + +def _origin_from_filename(filename: str) -> str: + file = (filename or "").replace("\\", "/") + if "/dpnp/" in file or file.startswith("dpnp/"): + return "dpnp" + if "/numpy/" in file or file.startswith("numpy/"): + return "numpy" + if "/dpctl/" in file or file.startswith("dpctl/"): + return "dpctl" + return "third_party" + + +def _json_dumps_one_line(obj) -> str: + return json.dumps(obj, separators=(",", ":")) + + +class DpnpInfraWarningsPlugin: + """Pytest custom plugin that records pytest-captured warnings. + + It only records what pytest already captures (via pytest_warning_recorded). + Does not change warnings filters. + + Env vars: + - DPNP_INFRA_WARNINGS_ENABLE=1 (enables the plugin) + - DPNP_INFRA_WARNINGS_DIRECTORY= (writes artifacts) + - DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT (optional filename) + - DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT (optional filename) + """ + + SUMMARY_BEGIN = "DPNP_WARNINGS_SUMMARY_BEGIN" + SUMMARY_END = "DPNP_WARNINGS_SUMMARY_END" + EVENT_PREFIX = "DPNP_WARNING_EVENT " + + def __init__(self): + self.enabled = _env_check("DPNP_INFRA_WARNINGS_ENABLE", default=False) + self.directory = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) + self.events_artifact = os.getenv( + "DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT", "dpnp_infra_warnings_events.jsonl" + ) + self.summary_artifact = os.getenv( + "DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT", "dpnp_infra_warnings_summary.json" + ) + + self.print_events = self.enabled + + self._counts = Counter() + self._examples = {} + self._totals = Counter() + self._env = {} + + self._events_fp = None + self._events_file = None + + def pytest_configure(self, config): + if not self.enabled: + return + + try: + numpy_version = numpy.__version__ + numpy_path = getattr(numpy, "__file__", "unknown") + dpnp_version = dpnp.__version__ + dpnp_path = getattr(dpnp, "__file__", "unknown") + dpctl_version = dpctl.__version__ + dpctl_path = getattr(dpctl, "__file__", "unknown") + except Exception: + numpy_version = "unknown" + numpy_path = "unknown" + dpnp_version = "unknown" + dpnp_path = "unknown" + dpctl_version = "unknown" + dpctl_path = "unknown" + + if self.directory: + os.makedirs(self.directory, exist_ok=True) + self._events_file = os.path.join(self.directory, self.events_artifact) + self._events_fp = open( + self._events_file, + "w", + encoding="utf-8", + buffering=1, + newline="\n", + ) + + self._env.update( + { + "numpy_version": numpy_version, + "numpy_path": numpy_path, + "dpnp_version": dpnp_version, + "dpnp_path": dpnp_path, + "dpctl_version": dpctl_version, + "dpctl_path": dpctl_path, + "job": os.getenv("JOB_NAME", "unknown"), + "build_number": os.getenv("BUILD_NUMBER", "unknown"), + "git_sha": os.getenv("GIT_COMMIT", "unknown"), + "events_file": self._events_file, + } + ) + + def pytest_warning_recorded(self, warning_message, when, nodeid, location): + if not self.enabled: + return + + category = getattr( + getattr(warning_message, "category", None), + "__name__", + str(getattr(warning_message, "category", "Warning")), + ) + message = str(getattr(warning_message, "message", warning_message)) + + filename = getattr(warning_message, "filename", None) or ( + location[0] if location and len(location) > 0 else None + ) + lineno = getattr(warning_message, "lineno", None) or ( + location[1] if location and len(location) > 1 else None + ) + func = location[2] if location and len(location) > 2 else None + + origin = _origin_from_filename(filename or "") + key = f"{category}||{origin}||{message}" + self._counts[key] += 1 + self._totals[f"category::{category}"] += 1 + self._totals[f"origin::{origin}"] += 1 + self._totals[f"phase::{when}"] += 1 + + if key not in self._examples: + self._examples[key] = { + "category": category, + "origin": origin, + "when": when, + "nodeid": nodeid, + "filename": filename, + "lineno": lineno, + "function": func, + "message": message, + } + + event = { + "when": when, + "nodeid": nodeid, + "category": category, + "origin": origin, + "message": message, + "filename": filename, + "lineno": lineno, + "function": func, + } + + if self._events_fp is not None: + try: + self._events_fp.write(_json_dumps_one_line(event) + "\n") + except Exception: + pass + + if self.print_events: + try: + sys.stderr.write(self.EVENT_PREFIX + _json_dumps_one_line(event) + "\n") + sys.stderr.flush() + except Exception: + pass + + def pytest_terminal_summary(self, terminalreporter, exitstatus, config): + if not self.enabled: + return + + summary = { + "schema_version": "1.0", + "exit_status": exitstatus, + "environment": dict(self._env), + "total_warning_events": int(sum(self._counts.values())), + "unique_warning_types": int(len(self._counts)), + "totals": dict(self._totals), + "top_unique_warnings": [ + dict(self._examples[k], count=c) + for k, c in self._counts.most_common(50) + if k in self._examples + ], + } + + if self.directory: + output_file = os.path.join(self.directory, self.summary_artifact) + try: + with open(output_file, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=2, sort_keys=True) + terminalreporter.write_line( + f"DPNP infrastructure warnings summary written to: {output_file}" + ) + except Exception as exc: + terminalreporter.write_line( + f"Failed to write DPNP infrastructure warnings summary to: {output_file}. Error: {exc}" + ) + + self._close_events_fp() + + terminalreporter.write_line(self.SUMMARY_BEGIN) + terminalreporter.write_line(_json_dumps_one_line(summary)) + terminalreporter.write_line(self.SUMMARY_END) + + def pytest_unconfigure(self, config): + self._close_events_fp() + + def _close_events_fp(self): + if self._events_fp is None: + return + try: + self._events_fp.close() + except Exception: + pass + self._events_fp = None + + +def register_infra_warnings_plugin_if_enabled(config) -> None: + """Register infra warnings plugin if enabled via env var.""" + + if not _env_check("DPNP_INFRA_WARNINGS_ENABLE"): + return + + plugin_name = "dpnp-infra-warnings" + if config.pluginmanager.get_plugin(plugin_name) is not None: + return + + config.pluginmanager.register(DpnpInfraWarningsPlugin(), plugin_name)