1-
21from __future__ import annotations
32
43import contextlib
76from typing import TYPE_CHECKING
87
98import pytest
10- from _pytest ._code .code import ExceptionRepr
9+ from _pytest ._code .code import ExceptionRepr , ReprEntry
1110from packaging import version
1211
1312if TYPE_CHECKING :
@@ -39,59 +38,65 @@ def pytest_runtest_makereport(item: Item, call): # noqa: ARG001
3938 return
4039
4140 if report .when == "call" and report .failed :
42- # collect information to be annotated
4341 filesystempath , lineno , _ = report .location
4442
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-
6343 if lineno is not None :
6444 # 0-index to 1-index
6545 lineno += 1
6646
67- # get the name of the current failed test, with parametrize info
6847 longrepr = report .head_line or item .name
6948
7049 # get the error message and line number from the actual error
7150 if isinstance (report .longrepr , ExceptionRepr ):
7251 if report .longrepr .reprcrash is not None :
7352 longrepr += "\n \n " + report .longrepr .reprcrash .message
7453 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 ]
7656 # 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+
7861 elif report .longrepr .reprcrash is not None :
7962 lineno = report .longrepr .reprcrash .lineno
8063 elif isinstance (report .longrepr , tuple ):
81- _ , lineno , message = report .longrepr
64+ filesystempath , lineno , message = report .longrepr
8265 longrepr += "\n \n " + message
8366 elif isinstance (report .longrepr , str ):
8467 longrepr += "\n \n " + report .longrepr
8568
8669 workflow_command = _build_workflow_command (
8770 "error" ,
88- filesystempath ,
71+ compute_path ( filesystempath ) ,
8972 lineno ,
9073 message = longrepr ,
9174 )
9275 print (workflow_command , file = sys .stderr )
9376
9477
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+
95100class _AnnotateWarnings :
96101 def pytest_warning_recorded (self , warning_message , when , nodeid , location ): # noqa: ARG002
97102 # enable only in a workflow of GitHub Actions
@@ -133,20 +138,21 @@ def pytest_addoption(parser):
133138 help = "Annotate failures in GitHub Actions." ,
134139 )
135140
141+
136142def pytest_configure (config ):
137143 if not config .option .exclude_warning_annotations :
138144 config .pluginmanager .register (_AnnotateWarnings (), "annotate_warnings" )
139145
140146
141147def _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 ,
150156):
151157 """Build a command to annotate a workflow."""
152158 result = f"::{ command_name } "
@@ -160,15 +166,13 @@ def _build_workflow_command(
160166 ("title" , title ),
161167 ]
162168
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 )
166170
167171 if message is not None :
168172 result = result + "::" + _escape (message )
169173
170174 return result
171175
172176
173- def _escape (s ) :
177+ def _escape (s : str ) -> str :
174178 return s .replace ("%" , "%25" ).replace ("\r " , "%0D" ).replace ("\n " , "%0A" )
0 commit comments