1
+ from __future__ import annotations
2
+ import sys
3
+ from enum import Enum , auto
4
+ from typing import Sequence
5
+
1
6
import pytest
2
7
3
8
from xdist .remote import Producer
@@ -251,14 +256,16 @@ def worker_collectionfinish(self, node, ids):
251
256
self ._session .testscollected = len (ids )
252
257
self .sched .add_node_collection (node , ids )
253
258
if self .terminal :
254
- self .trdist .setstatus (node .gateway .spec , "[%d]" % (len (ids )))
259
+ self .trdist .setstatus (
260
+ node .gateway .spec , WorkerStatus .CollectionDone , tests_collected = len (ids )
261
+ )
255
262
if self .sched .collection_is_completed :
256
263
if self .terminal and not self .sched .has_pending :
257
264
self .trdist .ensure_show_status ()
258
265
self .terminal .write_line ("" )
259
266
if self .config .option .verbose > 0 :
260
267
self .terminal .write_line (
261
- "scheduling tests via %s" % ( self .sched .__class__ .__name__ )
268
+ f "scheduling tests via { self .sched .__class__ .__name__ } "
262
269
)
263
270
self .sched .schedule ()
264
271
@@ -345,7 +352,7 @@ def _handlefailures(self, rep):
345
352
if rep .failed :
346
353
self .countfailures += 1
347
354
if self .maxfail and self .countfailures >= self .maxfail :
348
- self .shouldstop = "stopping after %d failures" % ( self .countfailures )
355
+ self .shouldstop = f "stopping after { self .countfailures } failures"
349
356
350
357
def triggershutdown (self ):
351
358
self .log ("triggering shutdown" )
@@ -372,32 +379,51 @@ def handle_crashitem(self, nodeid, worker):
372
379
self .config .hook .pytest_runtest_logreport (report = rep )
373
380
374
381
382
+ class WorkerStatus (Enum ):
383
+ """Status of each worker during creation/collection."""
384
+
385
+ # Worker spec has just been created.
386
+ Created = auto ()
387
+
388
+ # Worker has been initialized.
389
+ Initialized = auto ()
390
+
391
+ # Worker is now ready for collection.
392
+ ReadyForCollection = auto ()
393
+
394
+ # Worker has finished collection.
395
+ CollectionDone = auto ()
396
+
397
+
375
398
class TerminalDistReporter :
376
- def __init__ (self , config ):
399
+ def __init__ (self , config ) -> None :
377
400
self .config = config
378
401
self .tr = config .pluginmanager .getplugin ("terminalreporter" )
379
- self ._status = {}
402
+ self ._status : dict [ str , tuple [ WorkerStatus , int ]] = {}
380
403
self ._lastlen = 0
381
404
self ._isatty = getattr (self .tr , "isatty" , self .tr .hasmarkup )
382
405
383
- def write_line (self , msg ) :
406
+ def write_line (self , msg : str ) -> None :
384
407
self .tr .write_line (msg )
385
408
386
- def ensure_show_status (self ):
409
+ def ensure_show_status (self ) -> None :
387
410
if not self ._isatty :
388
411
self .write_line (self .getstatus ())
389
412
390
- def setstatus (self , spec , status , show = True ):
391
- self ._status [spec .id ] = status
413
+ def setstatus (
414
+ self , spec , status : WorkerStatus , * , tests_collected : int , show : bool = True
415
+ ) -> None :
416
+ self ._status [spec .id ] = (status , tests_collected )
392
417
if show and self ._isatty :
393
418
self .rewrite (self .getstatus ())
394
419
395
- def getstatus (self ):
420
+ def getstatus (self ) -> str :
396
421
if self .config .option .verbose >= 0 :
397
- parts = [f"{ spec .id } { self ._status [spec .id ]} " for spec in self ._specs ]
398
- return " / " .join (parts )
399
- else :
400
- return "bringing up nodes..."
422
+ line = get_workers_status_line (list (self ._status .values ()))
423
+ if line :
424
+ return line
425
+
426
+ return "bringing up nodes..."
401
427
402
428
def rewrite (self , line , newline = False ):
403
429
pline = line + " " * max (self ._lastlen - len (line ), 0 )
@@ -409,37 +435,41 @@ def rewrite(self, line, newline=False):
409
435
self .tr .rewrite (pline , bold = True )
410
436
411
437
@pytest .hookimpl
412
- def pytest_xdist_setupnodes (self , specs ):
438
+ def pytest_xdist_setupnodes (self , specs ) -> None :
413
439
self ._specs = specs
414
440
for spec in specs :
415
- self .setstatus (spec , "I" , show = False )
416
- self .setstatus (spec , "I" , show = True )
441
+ self .setstatus (spec , WorkerStatus . Created , tests_collected = 0 , show = False )
442
+ self .setstatus (spec , WorkerStatus . Created , tests_collected = 0 , show = True )
417
443
self .ensure_show_status ()
418
444
419
445
@pytest .hookimpl
420
- def pytest_xdist_newgateway (self , gateway ):
421
- if self .config .option .verbose > 0 :
422
- rinfo = gateway ._rinfo ()
446
+ def pytest_xdist_newgateway (self , gateway ) -> None :
447
+ rinfo = gateway ._rinfo ()
448
+ is_local = rinfo .executable == sys .executable
449
+ if self .config .option .verbose > 0 and not is_local :
423
450
version = "%s.%s.%s" % rinfo .version_info [:3 ]
424
451
self .rewrite (
425
452
"[%s] %s Python %s cwd: %s"
426
453
% (gateway .id , rinfo .platform , version , rinfo .cwd ),
427
454
newline = True ,
428
455
)
429
- self .setstatus (gateway .spec , "C" )
456
+ self .setstatus (gateway .spec , WorkerStatus . Initialized , tests_collected = 0 )
430
457
431
458
@pytest .hookimpl
432
- def pytest_testnodeready (self , node ):
433
- if self .config .option .verbose > 0 :
434
- d = node .workerinfo
459
+ def pytest_testnodeready (self , node ) -> None :
460
+ d = node .workerinfo
461
+ is_local = d .get ("executable" ) == sys .executable
462
+ if self .config .option .verbose > 0 and not is_local :
435
463
infoline = "[{}] Python {}" .format (
436
464
d ["id" ], d ["version" ].replace ("\n " , " -- " )
437
465
)
438
466
self .rewrite (infoline , newline = True )
439
- self .setstatus (node .gateway .spec , "ok" )
467
+ self .setstatus (
468
+ node .gateway .spec , WorkerStatus .ReadyForCollection , tests_collected = 0
469
+ )
440
470
441
471
@pytest .hookimpl
442
- def pytest_testnodedown (self , node , error ):
472
+ def pytest_testnodedown (self , node , error ) -> None :
443
473
if not error :
444
474
return
445
475
self .write_line (f"[{ node .gateway .id } ] node down: { error } " )
@@ -457,3 +487,36 @@ def get_default_max_worker_restart(config):
457
487
# if --max-worker-restart was not provided, use a reasonable default (#226)
458
488
result = config .option .numprocesses * 4
459
489
return result
490
+
491
+
492
+ def get_workers_status_line (
493
+ status_and_items : Sequence [tuple [WorkerStatus , int ]]
494
+ ) -> str :
495
+ """
496
+ Return the line to display during worker setup/collection based on the
497
+ status of the workers and number of tests collected for each.
498
+ """
499
+ statuses = [s for s , c in status_and_items ]
500
+ total_workers = len (statuses )
501
+ workers_noun = "worker" if total_workers == 1 else "workers"
502
+ if status_and_items and all (s == WorkerStatus .CollectionDone for s in statuses ):
503
+ # All workers collect the same number of items, so we grab
504
+ # the total number of items from the first worker.
505
+ first = status_and_items [0 ]
506
+ status , tests_collected = first
507
+ tests_noun = "item" if tests_collected == 1 else "items"
508
+ return f"{ total_workers } { workers_noun } [{ tests_collected } { tests_noun } ]"
509
+ if WorkerStatus .CollectionDone in statuses :
510
+ done = sum (1 for s , c in status_and_items if c > 0 )
511
+ return f"collecting: { done } /{ total_workers } { workers_noun } "
512
+ if WorkerStatus .ReadyForCollection in statuses :
513
+ ready = statuses .count (WorkerStatus .ReadyForCollection )
514
+ return f"ready: { ready } /{ total_workers } { workers_noun } "
515
+ if WorkerStatus .Initialized in statuses :
516
+ initialized = statuses .count (WorkerStatus .Initialized )
517
+ return f"initialized: { initialized } /{ total_workers } { workers_noun } "
518
+ if WorkerStatus .Created in statuses :
519
+ created = statuses .count (WorkerStatus .Created )
520
+ return f"created: { created } /{ total_workers } { workers_noun } "
521
+
522
+ return ""
0 commit comments