1
-
2
1
from __future__ import annotations
3
2
4
3
import contextlib
7
6
from typing import TYPE_CHECKING
8
7
9
8
import pytest
10
- from _pytest ._code .code import ExceptionRepr
9
+ from _pytest ._code .code import ExceptionRepr , ReprEntry
11
10
from packaging import version
12
11
13
12
if TYPE_CHECKING :
@@ -39,59 +38,65 @@ def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
39
38
return
40
39
41
40
if report .when == "call" and report .failed :
42
- # collect information to be annotated
43
41
filesystempath , lineno , _ = report .location
44
42
45
- runpath = os .environ .get ("PYTEST_RUN_PATH" )
46
- if runpath :
47
- filesystempath = os .path .join (runpath , filesystempath )
48
-
49
- # try to convert to absolute path in GitHub Actions
50
- workspace = os .environ .get ("GITHUB_WORKSPACE" )
51
- if workspace :
52
- full_path = os .path .abspath (filesystempath )
53
- try :
54
- rel_path = os .path .relpath (full_path , workspace )
55
- except ValueError :
56
- # os.path.relpath() will raise ValueError on Windows
57
- # when full_path and workspace have different mount points.
58
- # https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
59
- rel_path = filesystempath
60
- if not rel_path .startswith (".." ):
61
- filesystempath = rel_path
62
-
63
43
if lineno is not None :
64
44
# 0-index to 1-index
65
45
lineno += 1
66
46
67
- # get the name of the current failed test, with parametrize info
68
47
longrepr = report .head_line or item .name
69
48
70
49
# get the error message and line number from the actual error
71
50
if isinstance (report .longrepr , ExceptionRepr ):
72
51
if report .longrepr .reprcrash is not None :
73
52
longrepr += "\n \n " + report .longrepr .reprcrash .message
74
53
tb_entries = report .longrepr .reprtraceback .reprentries
75
- if len (tb_entries ) > 1 and tb_entries [0 ].reprfileloc is not None :
54
+ if tb_entries :
55
+ entry = tb_entries [0 ]
76
56
# Handle third-party exceptions
77
- lineno = tb_entries [0 ].reprfileloc .lineno
57
+ if isinstance (entry , ReprEntry ) and entry .reprfileloc is not None :
58
+ lineno = entry .reprfileloc .lineno
59
+ filesystempath = entry .reprfileloc .path
60
+
78
61
elif report .longrepr .reprcrash is not None :
79
62
lineno = report .longrepr .reprcrash .lineno
80
63
elif isinstance (report .longrepr , tuple ):
81
- _ , lineno , message = report .longrepr
64
+ filesystempath , lineno , message = report .longrepr
82
65
longrepr += "\n \n " + message
83
66
elif isinstance (report .longrepr , str ):
84
67
longrepr += "\n \n " + report .longrepr
85
68
86
69
workflow_command = _build_workflow_command (
87
70
"error" ,
88
- filesystempath ,
71
+ compute_path ( filesystempath ) ,
89
72
lineno ,
90
73
message = longrepr ,
91
74
)
92
75
print (workflow_command , file = sys .stderr )
93
76
94
77
78
+ def compute_path (filesystempath : str ) -> str :
79
+ """Extract and process location information from the report."""
80
+ runpath = os .environ .get ("PYTEST_RUN_PATH" )
81
+ if runpath :
82
+ filesystempath = os .path .join (runpath , filesystempath )
83
+
84
+ # try to convert to absolute path in GitHub Actions
85
+ workspace = os .environ .get ("GITHUB_WORKSPACE" )
86
+ if workspace :
87
+ full_path = os .path .abspath (filesystempath )
88
+ try :
89
+ rel_path = os .path .relpath (full_path , workspace )
90
+ except ValueError :
91
+ # os.path.relpath() will raise ValueError on Windows
92
+ # when full_path and workspace have different mount points.
93
+ rel_path = filesystempath
94
+ if not rel_path .startswith (".." ):
95
+ filesystempath = rel_path
96
+
97
+ return filesystempath
98
+
99
+
95
100
class _AnnotateWarnings :
96
101
def pytest_warning_recorded (self , warning_message , when , nodeid , location ): # noqa: ARG002
97
102
# enable only in a workflow of GitHub Actions
@@ -133,20 +138,21 @@ def pytest_addoption(parser):
133
138
help = "Annotate failures in GitHub Actions." ,
134
139
)
135
140
141
+
136
142
def pytest_configure (config ):
137
143
if not config .option .exclude_warning_annotations :
138
144
config .pluginmanager .register (_AnnotateWarnings (), "annotate_warnings" )
139
145
140
146
141
147
def _build_workflow_command (
142
- command_name ,
143
- file ,
144
- line ,
145
- end_line = None ,
146
- column = None ,
147
- end_column = None ,
148
- title = None ,
149
- message = None ,
148
+ command_name : str ,
149
+ file : str ,
150
+ line : int ,
151
+ end_line : int | None = None ,
152
+ column : int | None = None ,
153
+ end_column : int | None = None ,
154
+ title : str | None = None ,
155
+ message : str | None = None ,
150
156
):
151
157
"""Build a command to annotate a workflow."""
152
158
result = f"::{ command_name } "
@@ -160,15 +166,13 @@ def _build_workflow_command(
160
166
("title" , title ),
161
167
]
162
168
163
- result = result + "," .join (
164
- f"{ k } ={ v } " for k , v in entries if v is not None
165
- )
169
+ result = result + "," .join (f"{ k } ={ v } " for k , v in entries if v is not None )
166
170
167
171
if message is not None :
168
172
result = result + "::" + _escape (message )
169
173
170
174
return result
171
175
172
176
173
- def _escape (s ) :
177
+ def _escape (s : str ) -> str :
174
178
return s .replace ("%" , "%25" ).replace ("\r " , "%0D" ).replace ("\n " , "%0A" )
0 commit comments