Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dpnp/tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
3 changes: 3 additions & 0 deletions dpnp/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down Expand Up @@ -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]
Expand Down
213 changes: 213 additions & 0 deletions dpnp/tests/infra_warning_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import json
import os
import sys

from collections import Counter

import dpctl
import numpy

import dpnp
from . import config as warn_config


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=<dir> (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 = 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 = {}
self._totals = Counter()
self._env = {}

self._events_fp = None
self._events_file = None

def pytest_configure(self, config):
if not self.enabled:
return

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)
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",
)


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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to dump all events at once in pytest_terminal_summary to reduce pressure on the file system?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular line is writing the warnings to the artifact (events.jsonl) file whenever there is a warning.

The summary (summarized based of event.jsonl) will be all dumped at once to terminal and summary.json file at the end.

Summary written to terminal here - https://github.com/IntelPython/dpnp/pull/2757/changes#diff-e9924e662a511d3ac4d9aa5d26f9e9ac8865aad6a67b1dedbd9e24ce0079e590R213

Summary written to artifact here - https://github.com/IntelPython/dpnp/pull/2757/changes#diff-e9924e662a511d3ac4d9aa5d26f9e9ac8865aad6a67b1dedbd9e24ce0079e590R201

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular line is writing the warnings to the artifact (events.jsonl) file whenever there is a warning.

But we can do that 2 different ways:

  1. Write each warning to events.jsonl file once pytest_warning_recorded called (current approach as I can see)
  2. Add each warning reported by pytest_warning_recorded callback to a list (or whatever collection used) and write all of them at once inside pytest_terminal_summary callback

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already implemented. There are two different things written to terminal

  1. Actual warning{s) and
  2. The summary of all warnings.

The warnings are written to terminal in the same callback pytest_warning_recorded here - https://github.com/IntelPython/dpnp/pull/2757/changes#diff-e9924e662a511d3ac4d9aa5d26f9e9ac8865aad6a67b1dedbd9e24ce0079e590R174

In the pytest_terminal_summary callback only the summary of the total warnings is prepared and written to terminal at the end of the tests.

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:
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 bool(warn_config.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)