55from _colorize import decolor # type: ignore[import-not-found]
66from typing import TextIO
77
8- from .findtests import findtestdir
98from .result import TestResult
9+ lazy from .results import TestResults
1010from .runtests import RunTests
11- from .utils import StrPath , count
11+ from .utils import count
1212
1313
1414class Reporter :
@@ -17,6 +17,14 @@ class Reporter:
1717 The default reporter does nothing.
1818 """
1919
20+ def run_started (self , runtests : RunTests ) -> None :
21+ """Start a run of tests.
22+
23+ Called in the main process at the start of each run (the first run
24+ and the re-run), and in the process running the tests before each
25+ test file.
26+ """
27+
2028 def test_case_failed (self , test_case : str , traceback : str ,
2129 stream : TextIO ) -> None :
2230 """Report a failed test case.
@@ -25,11 +33,11 @@ def test_case_failed(self, test_case: str, traceback: str,
2533 test file in quiet mode.
2634 """
2735
28- def test_result (self , result : TestResult , runtests : RunTests ) -> None :
36+ def test_result (self , result : TestResult ) -> None :
2937 """Report the result of a test file, in the main process."""
3038
31- def run_failed (self , failed : list [ TestResult ] , state : str ) -> None :
32- """Report the test files which failed the run ."""
39+ def summary (self , results : TestResults , state : str ) -> None :
40+ """Report the results of all runs, in the main process ."""
3341
3442 @staticmethod
3543 def parse_traceback (traceback : str ) -> tuple [str | None , str | None , str ]:
@@ -49,25 +57,10 @@ def parse_traceback(traceback: str) -> tuple[str | None, str | None, str]:
4957 flags = re .MULTILINE )[- 1 ].strip ()
5058 return filename , line , exception
5159
52- @staticmethod
53- def test_filename (result : TestResult ,
54- test_dir : StrPath | None ) -> str | None :
55- """Get the filename of a test, or None if it is not found."""
56- if result .filename :
57- return result .filename
58- # The worker crashed or timed out before importing the test:
59- # "test.test_x.test_y" is test_x/test_y.py or test_x/test_y/__init__.py
60- name = result .test_name .removeprefix ("test." )
61- path = pathlib .Path (findtestdir (test_dir ), * name .split ("." ))
62- for filename in (path .with_suffix (".py" ), path / "__init__.py" ):
63- if filename .exists ():
64- return str (filename )
65- return None
66-
67-
68- def get_reporter (github_summary : str | None ) -> Reporter :
69- if github_summary :
70- return GitHubActionsReporter (github_summary )
60+
61+ def get_reporter () -> Reporter :
62+ if GitHubActionsReporter .enabled ():
63+ return GitHubActionsReporter ()
7164 return Reporter ()
7265
7366
@@ -79,23 +72,31 @@ class GitHubActionsReporter(Reporter):
7972 ESCAPE_DATA = str .maketrans ({"%" : "%25" , "\r " : "%0D" , "\n " : "%0A" })
8073 ESCAPE_PROPERTY = ESCAPE_DATA | str .maketrans ({":" : "%3A" , "," : "%2C" })
8174
82- def __init__ (self , summary_filename : str ) -> None :
83- self .summary_filename = summary_filename
75+ @staticmethod
76+ def enabled () -> bool :
77+ return "GITHUB_STEP_SUMMARY" in os .environ
78+
79+ def run_started (self , runtests : RunTests ) -> None :
80+ self .runtests = runtests
8481
8582 def test_case_failed (self , test_case : str , traceback : str ,
8683 stream : TextIO ) -> None :
84+ # Only report the failures which decide the result
85+ if not self .runtests .final_run :
86+ return
8787 # The job log has the full traceback: only keep the exception
8888 filename , line , exception = self .parse_traceback (decolor (traceback ))
8989 self .annotate (test_case , exception , filename , line , stream )
9090
91- def test_result (self , result : TestResult , runtests : RunTests ) -> None :
91+ def test_result (self , result : TestResult ) -> None :
9292 # Failed test cases are annotated by test_case_failed()
93- if (not result .is_failed (runtests .fail_env_changed )
93+ if (not self .runtests .final_run
94+ or not result .is_failed (self .runtests .fail_env_changed )
9495 or result .failed_test_cases ):
9596 return
9697 message = "\n " .join ([str (result ), * (result .env_changed_reasons or ())])
9798 self .annotate (result .test_name , decolor (message ),
98- self . test_filename ( result , runtests .test_dir ))
99+ result . find_filename ( self . runtests .test_dir ))
99100
100101 def annotate (self , title : str , message : str , filename : str | None ,
101102 line : str | None = None , stream : TextIO | None = None ) -> None :
@@ -113,7 +114,12 @@ def annotate(self, title: str, message: str, filename: str | None,
113114 print (f"::error { props_text } ::{ message .translate (self .ESCAPE_DATA )} " ,
114115 file = stream , flush = True )
115116
116- def run_failed (self , failed : list [TestResult ], state : str ) -> None :
117+ def summary (self , results : TestResults , state : str ) -> None :
118+ # Test files which failed in the last run (the re-run, if any)
119+ failed = [result for result in results .rerun_results
120+ if result .is_failed (self .runtests .fail_env_changed )]
121+ if not failed :
122+ return
117123 ncases = sum (len (result .failed_test_cases ) for result in failed )
118124 blocks = [f"## { decolor (state )} : "
119125 f"{ count (len (failed ), 'test file' )} and "
@@ -131,5 +137,6 @@ def run_failed(self, failed: list[TestResult], state: str) -> None:
131137 f"```pytb\n { decolor (traceback ).rstrip ()} \n ```" ,
132138 "</details>" ]
133139 # Markdown blocks are separated by an empty line
134- with open (self .summary_filename , "a" , encoding = "utf-8" ) as fp :
140+ with open (os .environ ["GITHUB_STEP_SUMMARY" ], "a" ,
141+ encoding = "utf-8" ) as fp :
135142 fp .write ("" .join (f"{ block } \n \n " for block in blocks ))
0 commit comments