From a6ee068a5ce23c428cebd5cf4103a4a927eb061d Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Thu, 5 Feb 2026 12:57:36 -0600 Subject: [PATCH 1/3] add pytest warning plugin --- dpnp/tests/conftest.py | 3 + dpnp/tests/infra_warning_utils.py | 239 ++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 dpnp/tests/infra_warning_utils.py diff --git a/dpnp/tests/conftest.py b/dpnp/tests/conftest.py index bd6c71f9a92b..5d766566bca5 100644 --- a/dpnp/tests/conftest.py +++ b/dpnp/tests/conftest.py @@ -44,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.") @@ -114,6 +115,8 @@ def pytest_configure(config): "ignore:invalid value encountered in arccosh:RuntimeWarning", ) + register_infra_warnings_plugin_if_enabled(config) + def pytest_collection_modifyitems(config, items): test_path = os.path.split(__file__)[0] diff --git a/dpnp/tests/infra_warning_utils.py b/dpnp/tests/infra_warning_utils.py new file mode 100644 index 000000000000..5f646d321427 --- /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._warnings = {} + 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._warnings: + self._warnings[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._warnings[k], count=c) + for k, c in self._counts.most_common(50) + if k in self._warnings + ], + } + + 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) From 34e751022c09a98f7d1b60a518cfb137b35e19ac Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Fri, 6 Feb 2026 11:01:03 -0600 Subject: [PATCH 2/3] pr improvements --- dpnp/tests/config.py | 12 +++++ dpnp/tests/infra_warning_utils.py | 80 +++++++++++-------------------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/dpnp/tests/config.py b/dpnp/tests/config.py index 8f7555a4ef62..62b60a9f0eae 100644 --- a/dpnp/tests/config.py +++ b/dpnp/tests/config.py @@ -4,3 +4,15 @@ float16_types = bool(os.getenv("DPNP_TEST_FLOAT_16", 0)) complex_types = bool(os.getenv("DPNP_TEST_COMPLEX_TYPES", 0)) bool_types = bool(os.getenv("DPNP_TEST_BOOL_TYPES", 0)) + + +infra_warnings_enable = os.getenv("DPNP_INFRA_WARNINGS_ENABLE", "0") == "1" +infra_warnings_directory = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) +infra_warnings_events_artifact = os.getenv( + "DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT", + "dpnp_infra_warnings_events.jsonl", +) +infra_warnings_summary_artifact = os.getenv( + "DPNP_INFRA_WARNINGS_SUMMARY_ARTIFACT", + "dpnp_infra_warnings_summary.json", +) diff --git a/dpnp/tests/infra_warning_utils.py b/dpnp/tests/infra_warning_utils.py index 5f646d321427..9c439517a835 100644 --- a/dpnp/tests/infra_warning_utils.py +++ b/dpnp/tests/infra_warning_utils.py @@ -8,13 +8,7 @@ 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"} +from . import config as warn_config def _origin_from_filename(filename: str) -> str: @@ -50,16 +44,10 @@ class DpnpInfraWarningsPlugin: 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.enabled = bool(warn_config.infra_warnings_enable) + self.directory = warn_config.infra_warnings_directory + self.events_artifact = warn_config.infra_warnings_events_artifact + self.summary_artifact = warn_config.infra_warnings_summary_artifact self._counts = Counter() self._warnings = {} @@ -72,21 +60,21 @@ def __init__(self): 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" + + self._env.update( + { + "numpy_version": getattr(numpy, "__version__", "unknown"), + "numpy_path": getattr(numpy, "__file__", "unknown"), + "dpnp_version": getattr(dpnp, "__version__", "unknown"), + "dpnp_path": getattr(dpnp, "__file__", "unknown"), + "dpctl_version": getattr(dpctl, "__version__", "unknown"), + "dpctl_path": getattr(dpctl, "__file__", "unknown"), + "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, + } + ) if self.directory: os.makedirs(self.directory, exist_ok=True) @@ -99,20 +87,6 @@ def pytest_configure(self, config): 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: @@ -169,12 +143,12 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): 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 + #Write the warnings to terminal + 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: @@ -229,7 +203,7 @@ def _close_events_fp(self): 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"): + if not bool(warn_config.infra_warnings_enable): return plugin_name = "dpnp-infra-warnings" From b209cabe4cbfc3289f82de32716f35c510533e6a Mon Sep 17 00:00:00 2001 From: Venkata Sai Gireesh Chamarthi Date: Mon, 9 Feb 2026 15:28:16 -0600 Subject: [PATCH 3/3] PR suggestions --- dpnp/tests/config.py | 2 +- dpnp/tests/infra_warning_utils.py | 61 ++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/dpnp/tests/config.py b/dpnp/tests/config.py index 62b60a9f0eae..a49fd8cad250 100644 --- a/dpnp/tests/config.py +++ b/dpnp/tests/config.py @@ -6,7 +6,7 @@ bool_types = bool(os.getenv("DPNP_TEST_BOOL_TYPES", 0)) -infra_warnings_enable = os.getenv("DPNP_INFRA_WARNINGS_ENABLE", "0") == "1" +infra_warnings_enable = bool(os.getenv("DPNP_INFRA_WARNINGS_ENABLE", 0)) infra_warnings_directory = os.getenv("DPNP_INFRA_WARNINGS_DIRECTORY", None) infra_warnings_events_artifact = os.getenv( "DPNP_INFRA_WARNINGS_EVENTS_ARTIFACT", diff --git a/dpnp/tests/infra_warning_utils.py b/dpnp/tests/infra_warning_utils.py index 9c439517a835..2650285e9a13 100644 --- a/dpnp/tests/infra_warning_utils.py +++ b/dpnp/tests/infra_warning_utils.py @@ -1,6 +1,7 @@ import json import os import sys +from pathlib import Path from collections import Counter @@ -41,7 +42,7 @@ class DpnpInfraWarningsPlugin: SUMMARY_BEGIN = "DPNP_WARNINGS_SUMMARY_BEGIN" SUMMARY_END = "DPNP_WARNINGS_SUMMARY_END" - EVENT_PREFIX = "DPNP_WARNING_EVENT " + EVENT_PREFIX = "DPNP_WARNING_EVENT - " def __init__(self): self.enabled = bool(warn_config.infra_warnings_enable) @@ -57,10 +58,17 @@ def __init__(self): self._events_fp = None self._events_file = None - def pytest_configure(self, config): + def _log_stdout(self, message: str) -> None: + try: + sys.stderr.write(message.rstrip("\n") + "\n") + sys.stderr.flush() + except Exception: + pass + + def pytest_configure(self, _config): if not self.enabled: return - + self._env.update( { "numpy_version": getattr(numpy, "__version__", "unknown"), @@ -72,20 +80,36 @@ def pytest_configure(self, config): "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, } ) 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", - ) + try: + p = Path(self.directory).expanduser().resolve() + if p.exists() and not p.is_dir(): + raise ValueError(f"{p} exists and is not a directory") + + p.mkdir(parents=True, exist_ok=True) + + if (not self.events_artifact + or Path(self.events_artifact).name != self.events_artifact + ): + raise ValueError( + "Invalid events artifact filename (must not contain path parts)" + ) + + self._events_file = p / self.events_artifact + self._events_fp = self._events_file.open( + mode="w", encoding="utf-8", buffering=1, newline="\n" + ) + except Exception as exc: + self._events_fp = None + self._events_file = None + self.directory = None + self._log_stdout( + "DPNP infra warnings plugin: artifacts disabled " + f"(failed to initialize directory/files): {exc}" + ) def pytest_warning_recorded(self, warning_message, when, nodeid, location): @@ -143,14 +167,9 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): except Exception: pass - #Write the warnings to terminal - try: - sys.stderr.write(self.EVENT_PREFIX + _json_dumps_one_line(event) + "\n") - sys.stderr.flush() - except Exception: - pass + self._log_stdout(f"{self.EVENT_PREFIX} {_json_dumps_one_line(event)}") - def pytest_terminal_summary(self, terminalreporter, exitstatus, config): + def pytest_terminal_summary(self, terminalreporter, exitstatus, _config): if not self.enabled: return @@ -187,7 +206,7 @@ def pytest_terminal_summary(self, terminalreporter, exitstatus, config): terminalreporter.write_line(_json_dumps_one_line(summary)) terminalreporter.write_line(self.SUMMARY_END) - def pytest_unconfigure(self, config): + def pytest_unconfigure(self, _config): self._close_events_fp() def _close_events_fp(self):