3
3
from typing import NamedTuple
4
4
5
5
import pytest
6
+ from _pytest .terminal import TerminalReporter
7
+
8
+ ARGPARSE_PARSER_GROUP = "pytest-xdist-worker-stats"
9
+ ARGPARSE_REPORT_WORKER_RUNTIMES_OPTION_NAME = "pytest_xdist_worker_stats_report_worker_runtimes"
10
+ ARGPARSE_REPORT_TEST_BREAKDOWN_OPTION_NAME = "pytest_xdist_worker_stats_report_test_breakdown"
6
11
7
12
SHARED_WORKER_INFO = "worker_info"
8
13
9
14
10
- class RunStatistics (NamedTuple ):
15
+ class RuntimeStats (NamedTuple ):
11
16
mininum_tests : int
12
17
maximum_tests : int
13
18
average_tests : float
@@ -17,10 +22,12 @@ class RunStatistics(NamedTuple):
17
22
18
23
19
24
class XdistWorkerStatsPlugin :
20
- def __init__ (self , config ):
25
+ def __init__ (self , config : pytest . Config ):
21
26
self .config = config
22
27
self .test_stats = {}
23
- self .worker_test_times = {}
28
+ self .worker_stats = {}
29
+ self .report_worker_runtimes = config .getoption (ARGPARSE_REPORT_WORKER_RUNTIMES_OPTION_NAME , False )
30
+ self .report_test_breakdown = config .getoption (ARGPARSE_REPORT_TEST_BREAKDOWN_OPTION_NAME , False )
24
31
25
32
def add (self , name ):
26
33
self .test_stats [name ] = self .test_stats .get (name ) or {}
@@ -32,52 +39,57 @@ def pytest_runtest_setup(self, item):
32
39
@pytest .hookimpl (hookwrapper = True )
33
40
def pytest_runtest_call (self , item ):
34
41
yield
35
- end = time .time ()
36
- self .add (item .nodeid )["diff" ] = end - self .add (item .nodeid )["start" ]
37
-
38
- if (worker := os .environ .get ("PYTEST_XDIST_WORKER" , "primary" )) not in self .worker_test_times :
39
- self .worker_test_times [worker ] = []
40
-
41
- self .worker_test_times [worker ].append (self .add (item .nodeid )["diff" ])
42
-
43
- def get_statistics (self ) -> RunStatistics :
44
- workers = self .worker_test_times .keys ()
45
- tests = [len (self .worker_test_times [worker ]) for worker in workers ]
46
- runtimes = [sum (self .worker_test_times [worker ]) for worker in workers ]
47
-
48
- return RunStatistics (
49
- mininum_tests = min (tests ),
50
- maximum_tests = max (tests ),
51
- average_tests = sum (tests ) / len (tests ),
52
- mininum_runtime = min (runtimes ),
53
- maximum_runtime = max (runtimes ),
54
- average_runtime = sum (runtimes ) / len (runtimes ),
42
+ runtime = time .time () - self .add (item .nodeid )["start" ]
43
+ self .add (item .nodeid )["runtime" ] = runtime
44
+
45
+ if (worker := os .environ .get ("PYTEST_XDIST_WORKER" , "primary" )) not in self .worker_stats :
46
+ self .worker_stats [worker ] = {}
47
+
48
+ self .worker_stats [worker ][item .nodeid ] = runtime
49
+
50
+ def get_runtime_stats (self ) -> RuntimeStats :
51
+ test_counts = [len (stats ) for stats in self .worker_stats .values ()]
52
+ test_runtimes = [sum (stats .values ()) for stats in self .worker_stats .values ()]
53
+
54
+ return RuntimeStats (
55
+ mininum_tests = min (test_counts ),
56
+ maximum_tests = max (test_counts ),
57
+ average_tests = sum (test_counts ) / len (test_counts ),
58
+ mininum_runtime = min (test_runtimes ),
59
+ maximum_runtime = max (test_runtimes ),
60
+ average_runtime = sum (test_runtimes ) / len (test_runtimes ),
55
61
)
56
62
57
- def pytest_terminal_summary (self , terminalreporter ):
63
+ def pytest_terminal_summary (self , terminalreporter : TerminalReporter ):
58
64
"""
59
65
If there's multiple workers, report on number of tests and total runtime.
60
66
"""
61
67
tr = terminalreporter
62
- if self .worker_test_times and len (self .worker_test_times ) > 1 :
68
+ if self .worker_stats and len (self .worker_stats ) > 1 :
63
69
tr ._tw .sep ("=" , "Worker statistics" , yellow = True )
64
- workers = sorted (self .worker_test_times .keys (), key = lambda x : int (x .lstrip ("gw" )))
65
- statistics = self .get_statistics ()
66
-
67
- for worker in workers :
68
- worker_times = self .worker_test_times [worker ]
69
- tr ._tw .line (f"worker { worker : <5} : { len (worker_times ): >4} tests { sum (worker_times ):10.2f} s runtime" )
70
-
71
- tr ._tw .line ("" )
70
+ worker_columns = len (max (self .worker_stats .keys (), key = len )) + 2
71
+
72
+ if self .report_worker_runtimes :
73
+ for worker , stats in sorted (self .worker_stats .items ()):
74
+ runtimes = stats .values ()
75
+ tr ._tw .line (
76
+ f"worker { worker : <{worker_columns }} : { len (runtimes ): >4} tests { sum (runtimes ):10.2f} s runtime"
77
+ )
78
+ if self .report_test_breakdown :
79
+ for nodeid in sorted (stats .keys ()):
80
+ tr ._tw .line (f" { nodeid } " )
81
+ tr ._tw .line ("" )
82
+
83
+ runtime_stats = self .get_runtime_stats ()
72
84
tr ._tw .line (
73
- f"Tests : min { statistics .mininum_tests : >8} , "
74
- f"max { statistics .maximum_tests : >8} , "
75
- f"average { statistics .average_tests :.1f} "
85
+ f"Tests : min { runtime_stats .mininum_tests : >8} , "
86
+ f"max { runtime_stats .maximum_tests : >8} , "
87
+ f"average { runtime_stats .average_tests :.1f} "
76
88
)
77
89
tr ._tw .line (
78
- f"Runtime : min { statistics .mininum_runtime :7.2f} s, "
79
- f"max { statistics .maximum_runtime :7.2f} s, "
80
- f"average { statistics .average_runtime :.2f} s"
90
+ f"Runtime : min { runtime_stats .mininum_runtime :7.2f} s, "
91
+ f"max { runtime_stats .maximum_runtime :7.2f} s, "
92
+ f"average { runtime_stats .average_runtime :.2f} s"
81
93
)
82
94
83
95
def pytest_testnodedown (self , node , error ):
@@ -88,7 +100,7 @@ def pytest_testnodedown(self, node, error):
88
100
hasattr (node , "workeroutput" )
89
101
and (node_worker_stats := node .workeroutput .get (SHARED_WORKER_INFO )) is not None
90
102
):
91
- self .worker_test_times .update (dict (node_worker_stats ))
103
+ self .worker_stats .update (dict (node_worker_stats ))
92
104
93
105
@pytest .hookimpl (hookwrapper = True , trylast = True )
94
106
def pytest_sessionfinish (self , session , exitstatus ):
@@ -98,4 +110,4 @@ def pytest_sessionfinish(self, session, exitstatus):
98
110
"""
99
111
yield
100
112
if hasattr (self .config , "workeroutput" ):
101
- self .config .workeroutput [SHARED_WORKER_INFO ] = self .worker_test_times
113
+ self .config .workeroutput [SHARED_WORKER_INFO ] = self .worker_stats
0 commit comments