@@ -69,10 +69,20 @@ def test_cancel_event_independent(self):
6969
7070
7171class TestTransferServiceInit :
72- def test_starts_daemon_worker_thread (self ):
72+ def test_starts_daemon_worker_threads (self ):
7373 svc = TransferService (notify_window = None )
74- assert svc ._worker .is_alive ()
75- assert svc ._worker .daemon is True
74+ assert len (svc ._workers ) == 1
75+ assert all (t .is_alive () for t in svc ._workers )
76+ assert all (t .daemon for t in svc ._workers )
77+
78+ def test_starts_multiple_workers (self ):
79+ svc = TransferService (notify_window = None , max_workers = 3 )
80+ assert len (svc ._workers ) == 3
81+ assert all (t .is_alive () for t in svc ._workers )
82+
83+ def test_max_workers_clamped_to_one (self ):
84+ svc = TransferService (notify_window = None , max_workers = 0 )
85+ assert len (svc ._workers ) == 1
7686
7787 def test_jobs_returns_snapshot (self ):
7888 svc = TransferService (notify_window = None )
@@ -444,3 +454,61 @@ def test_status_values(self):
444454 assert TransferStatus .COMPLETE .value == "complete"
445455 assert TransferStatus .FAILED .value == "failed"
446456 assert TransferStatus .CANCELLED .value == "cancelled"
457+
458+
459+ # ---------------------------------------------------------------------------
460+ # Concurrent worker pool
461+ # ---------------------------------------------------------------------------
462+
463+
464+ class TestConcurrentWorkers :
465+ def test_jobs_run_concurrently_with_multiple_workers (self ):
466+ """Two slow jobs should overlap when max_workers >= 2."""
467+ barrier = threading .Barrier (2 , timeout = 5 )
468+ completed_order : list [str ] = []
469+ lock = threading .Lock ()
470+
471+ mock_client = MagicMock ()
472+
473+ def slow_download (src , fh , callback = None ):
474+ name = PurePosixPath (src ).name
475+ barrier .wait () # both workers must reach here before either proceeds
476+ with lock :
477+ completed_order .append (name )
478+
479+ mock_client .download .side_effect = slow_download
480+
481+ svc = TransferService (notify_window = None , max_workers = 2 )
482+ with patch ("builtins.open" , return_value = MagicMock (spec = io .BufferedWriter )):
483+ j1 = svc .submit_download (mock_client , "/r/a.txt" , "/tmp/a.txt" )
484+ j2 = svc .submit_download (mock_client , "/r/b.txt" , "/tmp/b.txt" )
485+ _wait_for_terminal (j1 )
486+ _wait_for_terminal (j2 )
487+
488+ assert j1 .status == TransferStatus .COMPLETE
489+ assert j2 .status == TransferStatus .COMPLETE
490+ assert len (completed_order ) == 2
491+
492+ def test_set_max_workers_increases_pool (self ):
493+ svc = TransferService (notify_window = None , max_workers = 1 )
494+ assert len ([t for t in svc ._workers if t .is_alive ()]) == 1
495+
496+ svc .set_max_workers (3 )
497+ time .sleep (0.1 )
498+ alive = [t for t in svc ._workers if t .is_alive ()]
499+ assert len (alive ) == 3
500+
501+ def test_set_max_workers_decreases_pool (self ):
502+ svc = TransferService (notify_window = None , max_workers = 3 )
503+ assert len (svc ._workers ) == 3
504+
505+ svc .set_max_workers (1 )
506+ # Give sentinels time to be consumed
507+ time .sleep (0.5 )
508+ alive = [t for t in svc ._workers if t .is_alive ()]
509+ assert len (alive ) == 1
510+
511+ def test_set_max_workers_clamps_to_one (self ):
512+ svc = TransferService (notify_window = None , max_workers = 2 )
513+ svc .set_max_workers (0 )
514+ assert svc ._max_workers == 1
0 commit comments