@@ -247,6 +247,8 @@ def __init__(self, config: Config, file=None) -> None:
247247 self ._showfspath = None
248248
249249 self .stats = {} # type: Dict[str, List[Any]]
250+ self ._main_color = None # type: Optional[str]
251+ self ._known_types = None # type: Optional[List]
250252 self .startdir = config .invocation_dir
251253 if file is None :
252254 file = sys .stdout
@@ -365,6 +367,12 @@ def section(self, title, sep="=", **kw):
365367 def line (self , msg , ** kw ):
366368 self ._tw .line (msg , ** kw )
367369
370+ def _add_stats (self , category : str , items : List ) -> None :
371+ set_main_color = category not in self .stats
372+ self .stats .setdefault (category , []).extend (items [:])
373+ if set_main_color :
374+ self ._set_main_color ()
375+
368376 def pytest_internalerror (self , excrepr ):
369377 for line in str (excrepr ).split ("\n " ):
370378 self .write_line ("INTERNALERROR> " + line )
@@ -374,15 +382,14 @@ def pytest_warning_captured(self, warning_message, item):
374382 # from _pytest.nodes import get_fslocation_from_item
375383 from _pytest .warnings import warning_record_to_str
376384
377- warnings = self .stats .setdefault ("warnings" , [])
378385 fslocation = warning_message .filename , warning_message .lineno
379386 message = warning_record_to_str (warning_message )
380387
381388 nodeid = item .nodeid if item is not None else ""
382389 warning_report = WarningReport (
383390 fslocation = fslocation , message = message , nodeid = nodeid
384391 )
385- warnings . append ( warning_report )
392+ self . _add_stats ( "warnings" , [ warning_report ] )
386393
387394 def pytest_plugin_registered (self , plugin ):
388395 if self .config .option .traceconfig :
@@ -393,7 +400,7 @@ def pytest_plugin_registered(self, plugin):
393400 self .write_line (msg )
394401
395402 def pytest_deselected (self , items ):
396- self .stats . setdefault ("deselected" , []). extend ( items )
403+ self ._add_stats ("deselected" , items )
397404
398405 def pytest_runtest_logstart (self , nodeid , location ):
399406 # ensure that the path is printed before the
@@ -414,7 +421,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
414421 word , markup = word
415422 else :
416423 markup = None
417- self .stats . setdefault (category , []). append ( rep )
424+ self ._add_stats (category , [rep ] )
418425 if not letter and not word :
419426 # probably passed setup/teardown
420427 return
@@ -456,6 +463,10 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
456463 self ._tw .write (" " + line )
457464 self .currentfspath = - 2
458465
466+ @property
467+ def _is_last_item (self ):
468+ return len (self ._progress_nodeids_reported ) == self ._session .testscollected
469+
459470 def pytest_runtest_logfinish (self , nodeid ):
460471 assert self ._session
461472 if self .verbosity <= 0 and self ._show_progress_info :
@@ -465,15 +476,12 @@ def pytest_runtest_logfinish(self, nodeid):
465476 else :
466477 progress_length = len (" [100%]" )
467478
468- main_color , _ = _get_main_color (self .stats )
469-
470479 self ._progress_nodeids_reported .add (nodeid )
471- is_last_item = (
472- len (self ._progress_nodeids_reported ) == self ._session .testscollected
473- )
474- if is_last_item :
475- self ._write_progress_information_filling_space (color = main_color )
480+
481+ if self ._is_last_item :
482+ self ._write_progress_information_filling_space ()
476483 else :
484+ main_color , _ = self ._get_main_color ()
477485 w = self ._width_of_current_line
478486 past_edge = w + progress_length + 1 >= self ._screen_width
479487 if past_edge :
@@ -497,9 +505,8 @@ def _get_progress_information_message(self) -> str:
497505 )
498506 return " [100%]"
499507
500- def _write_progress_information_filling_space (self , color = None ):
501- if not color :
502- color , _ = _get_main_color (self .stats )
508+ def _write_progress_information_filling_space (self ):
509+ color , _ = self ._get_main_color ()
503510 msg = self ._get_progress_information_message ()
504511 w = self ._width_of_current_line
505512 fill = self ._tw .fullwidth - w - 1
@@ -524,9 +531,9 @@ def pytest_collection(self):
524531
525532 def pytest_collectreport (self , report : CollectReport ) -> None :
526533 if report .failed :
527- self .stats . setdefault ("error" , []). append ( report )
534+ self ._add_stats ("error" , [report ] )
528535 elif report .skipped :
529- self .stats . setdefault ("skipped" , []). append ( report )
536+ self ._add_stats ("skipped" , [report ] )
530537 items = [x for x in report .result if isinstance (x , pytest .Item )]
531538 self ._numcollected += len (items )
532539 if self .isatty :
@@ -909,7 +916,7 @@ def summary_stats(self):
909916 return
910917
911918 session_duration = time .time () - self ._sessionstarttime
912- (parts , main_color ) = build_summary_stats_line ( self .stats )
919+ (parts , main_color ) = self .build_summary_stats_line ( )
913920 line_parts = []
914921
915922 display_sep = self .verbosity >= 0
@@ -1012,6 +1019,56 @@ def show_skipped(lines: List[str]) -> None:
10121019 for line in lines :
10131020 self .write_line (line )
10141021
1022+ def _get_main_color (self ) -> Tuple [str , List [str ]]:
1023+ if self ._main_color is None or self ._known_types is None or self ._is_last_item :
1024+ self ._set_main_color ()
1025+ assert self ._main_color
1026+ assert self ._known_types
1027+ return self ._main_color , self ._known_types
1028+
1029+ def _set_main_color (self ) -> Tuple [str , List [str ]]:
1030+ stats = self .stats
1031+ known_types = (
1032+ "failed passed skipped deselected xfailed xpassed warnings error" .split ()
1033+ )
1034+ unknown_type_seen = False
1035+ for found_type in stats .keys ():
1036+ if found_type not in known_types :
1037+ if found_type : # setup/teardown reports have an empty key, ignore them
1038+ known_types .append (found_type )
1039+ unknown_type_seen = True
1040+
1041+ # main color
1042+ if "failed" in stats or "error" in stats :
1043+ main_color = "red"
1044+ elif "warnings" in stats or "xpassed" in stats or unknown_type_seen :
1045+ main_color = "yellow"
1046+ elif "passed" in stats or not self ._is_last_item :
1047+ main_color = "green"
1048+ else :
1049+ main_color = "yellow"
1050+ self ._main_color , self ._known_types = main_color , known_types
1051+ return main_color , known_types
1052+
1053+ def build_summary_stats_line (self ) -> Tuple [List [Tuple [str , Dict [str , bool ]]], str ]:
1054+ main_color , known_types = self ._get_main_color ()
1055+
1056+ parts = []
1057+ for key in known_types :
1058+ reports = self .stats .get (key , None )
1059+ if reports :
1060+ count = sum (
1061+ 1 for rep in reports if getattr (rep , "count_towards_summary" , True )
1062+ )
1063+ color = _color_for_type .get (key , _color_for_type_default )
1064+ markup = {color : True , "bold" : color == main_color }
1065+ parts .append (("%d %s" % _make_plural (count , key ), markup ))
1066+
1067+ if not parts :
1068+ parts = [("no tests ran" , {_color_for_type_default : True })]
1069+
1070+ return parts , main_color
1071+
10151072
10161073def _get_pos (config , rep ):
10171074 nodeid = config .cwd_relative_nodeid (rep .nodeid )
@@ -1100,50 +1157,6 @@ def _make_plural(count, noun):
11001157 return count , noun + "s" if count != 1 else noun
11011158
11021159
1103- def _get_main_color (stats ) -> Tuple [str , List [str ]]:
1104- known_types = (
1105- "failed passed skipped deselected xfailed xpassed warnings error" .split ()
1106- )
1107- unknown_type_seen = False
1108- for found_type in stats .keys ():
1109- if found_type not in known_types :
1110- if found_type : # setup/teardown reports have an empty key, ignore them
1111- known_types .append (found_type )
1112- unknown_type_seen = True
1113-
1114- # main color
1115- if "failed" in stats or "error" in stats :
1116- main_color = "red"
1117- elif "warnings" in stats or unknown_type_seen :
1118- main_color = "yellow"
1119- elif "passed" in stats :
1120- main_color = "green"
1121- else :
1122- main_color = "yellow"
1123-
1124- return main_color , known_types
1125-
1126-
1127- def build_summary_stats_line (stats ):
1128- main_color , known_types = _get_main_color (stats )
1129-
1130- parts = []
1131- for key in known_types :
1132- reports = stats .get (key , None )
1133- if reports :
1134- count = sum (
1135- 1 for rep in reports if getattr (rep , "count_towards_summary" , True )
1136- )
1137- color = _color_for_type .get (key , _color_for_type_default )
1138- markup = {color : True , "bold" : color == main_color }
1139- parts .append (("%d %s" % _make_plural (count , key ), markup ))
1140-
1141- if not parts :
1142- parts = [("no tests ran" , {_color_for_type_default : True })]
1143-
1144- return parts , main_color
1145-
1146-
11471160def _plugin_nameversions (plugininfo ) -> List [str ]:
11481161 values = [] # type: List[str]
11491162 for plugin , dist in plugininfo :
0 commit comments