4
4
import os
5
5
import sys
6
6
from collections import OrderedDict
7
- from typing import TYPE_CHECKING
8
7
9
8
import pytest
10
9
from _pytest ._code .code import ExceptionRepr
11
10
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" )
15
17
16
18
17
19
# Reference:
18
20
# https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
19
21
# 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
21
23
#
22
24
# Inspired by:
23
25
# https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
24
26
25
27
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
+ )
85
86
86
87
87
88
def _error_workflow_command (filesystempath , lineno , longrepr ):
@@ -102,3 +103,21 @@ def _error_workflow_command(filesystempath, lineno, longrepr):
102
103
103
104
def _escape (s ):
104
105
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