44import os
55import sys
66from collections import OrderedDict
7- from typing import TYPE_CHECKING
87
98import pytest
109from _pytest ._code .code import ExceptionRepr
1110
12- if TYPE_CHECKING :
13- from _pytest .nodes import Item
14- from _pytest .reports import CollectReport
11+ try :
12+ from xdist import is_xdist_worker
13+
14+ except ImportError :
15+ def is_xdist_worker (request_or_session ):
16+ return hasattr (request_or_session .config , "workerinput" )
1517
1618
1719# Reference:
1820# https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
1921# https://docs.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example
20- # https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_makereport
22+ # https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_logreport
2123#
2224# Inspired by:
2325# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
2426
2527
26- @pytest .hookimpl (tryfirst = True , hookwrapper = True )
27- def pytest_runtest_makereport (item : Item , call ): # noqa: ARG001
28- # execute all other hooks to obtain the report object
29- outcome = yield
30- report : CollectReport = outcome .get_result ()
31-
32- # enable only in a workflow of GitHub Actions
33- # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
34- if os .environ .get ("GITHUB_ACTIONS" ) != "true" :
35- return
36-
37- if report .when == "call" and report .failed :
38- # collect information to be annotated
39- filesystempath , lineno , _ = report .location
40-
41- runpath = os .environ .get ("PYTEST_RUN_PATH" )
42- if runpath :
43- filesystempath = os .path .join (runpath , filesystempath )
44-
45- # try to convert to absolute path in GitHub Actions
46- workspace = os .environ .get ("GITHUB_WORKSPACE" )
47- if workspace :
48- full_path = os .path .abspath (filesystempath )
49- try :
50- rel_path = os .path .relpath (full_path , workspace )
51- except ValueError :
52- # os.path.relpath() will raise ValueError on Windows
53- # when full_path and workspace have different mount points.
54- # https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
55- rel_path = filesystempath
56- if not rel_path .startswith (".." ):
57- filesystempath = rel_path
58-
59- if lineno is not None :
60- # 0-index to 1-index
61- lineno += 1
62-
63- # get the name of the current failed test, with parametrize info
64- longrepr = report .head_line or item .name
65-
66- # get the error message and line number from the actual error
67- if isinstance (report .longrepr , ExceptionRepr ):
68- if report .longrepr .reprcrash is not None :
69- longrepr += "\n \n " + report .longrepr .reprcrash .message
70- tb_entries = report .longrepr .reprtraceback .reprentries
71- if len (tb_entries ) > 1 and tb_entries [0 ].reprfileloc is not None :
72- # Handle third-party exceptions
73- lineno = tb_entries [0 ].reprfileloc .lineno
74- elif report .longrepr .reprcrash is not None :
75- lineno = report .longrepr .reprcrash .lineno
76- elif isinstance (report .longrepr , tuple ):
77- _ , lineno , message = report .longrepr
78- longrepr += "\n \n " + message
79- elif isinstance (report .longrepr , str ):
80- longrepr += "\n \n " + report .longrepr
81-
82- print (
83- _error_workflow_command (filesystempath , lineno , longrepr ), file = sys .stderr
84- )
28+ class Reporter :
29+ def pytest_runtest_logreport (self , report : pytest .TestReport ):
30+ if report .when == "call" and report .failed :
31+ # collect information to be annotated
32+ filesystempath , lineno , domaininfo = report .location
33+
34+ runpath = os .environ .get ("PYTEST_RUN_PATH" )
35+ if runpath :
36+ filesystempath = os .path .join (runpath , filesystempath )
37+
38+ # try to convert to absolute path in GitHub Actions
39+ workspace = os .environ .get ("GITHUB_WORKSPACE" )
40+ if workspace :
41+ full_path = os .path .abspath (filesystempath )
42+ try :
43+ rel_path = os .path .relpath (full_path , workspace )
44+ except ValueError :
45+ # os.path.relpath() will raise ValueError on Windows
46+ # when full_path and workspace have different mount points.
47+ # https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
48+ rel_path = filesystempath
49+ if not rel_path .startswith (".." ):
50+ filesystempath = rel_path
51+
52+ if lineno is not None :
53+ # 0-index to 1-index
54+ lineno += 1
55+
56+ # get the name of the current failed test, with parametrize info
57+ longrepr = getattr (report , 'head_line' , None )
58+
59+ if not longrepr :
60+ # BaseReport.head_line currently does this
61+ longrepr = domaininfo
62+
63+ if not longrepr :
64+ # Should not happen
65+ longrepr = _remove_prefix (report .nodeid , f'{ report .fspath } ::' )
66+
67+ # get the error message and line number from the actual error
68+ if isinstance (report .longrepr , ExceptionRepr ):
69+ if report .longrepr .reprcrash is not None :
70+ longrepr += "\n \n " + report .longrepr .reprcrash .message
71+ tb_entries = report .longrepr .reprtraceback .reprentries
72+ if len (tb_entries ) > 1 and tb_entries [0 ].reprfileloc is not None :
73+ # Handle third-party exceptions
74+ lineno = tb_entries [0 ].reprfileloc .lineno
75+ elif report .longrepr .reprcrash is not None :
76+ lineno = report .longrepr .reprcrash .lineno
77+ elif isinstance (report .longrepr , tuple ):
78+ _ , lineno , message = report .longrepr
79+ longrepr += "\n \n " + message
80+ elif isinstance (report .longrepr , str ):
81+ longrepr += "\n \n " + report .longrepr
82+
83+ print (
84+ _error_workflow_command (filesystempath , lineno , longrepr ), file = sys .stderr
85+ )
8586
8687
8788def _error_workflow_command (filesystempath , lineno , longrepr ):
@@ -102,3 +103,21 @@ def _error_workflow_command(filesystempath, lineno, longrepr):
102103
103104def _escape (s ):
104105 return s .replace ("%" , "%25" ).replace ("\r " , "%0D" ).replace ("\n " , "%0A" )
106+
107+
108+ def _remove_prefix (s , prefix ):
109+ # Replace with built-in `.removeprefix()` when Python 3.8 support is dropped
110+ return s [len (prefix ):] if s .startswith (prefix ) else s
111+
112+
113+ def pytest_sessionstart (session : pytest .Session ):
114+ # enable only in a workflow of GitHub Actions
115+ # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
116+ if os .environ .get ("GITHUB_ACTIONS" ) != "true" :
117+ return
118+
119+ # print commands only from the main xdist process
120+ if is_xdist_worker (session ):
121+ return
122+
123+ session .config .pluginmanager .register (Reporter ())
0 commit comments