22import threading
33from datetime import datetime
44from pathlib import Path
5+ from unittest import mock
56from unittest .mock import MagicMock
67
78import pytest
@@ -38,8 +39,6 @@ def test_rsyncer_initialises(
3839 # Assign values to parameters
3940 basepath_local = tmp_path / "local"
4041 basepath_remote = tmp_path / "remote"
41- do_transfer = True
42- remove_files = True
4342
4443 # Create a test substrings blacklist dict
4544 substrings_blacklist = {
@@ -58,8 +57,6 @@ def test_rsyncer_initialises(
5857 server_url = mock_server_url ,
5958 stop_callback = dummy_callback ,
6059 local = is_local ,
61- do_transfer = do_transfer ,
62- remove_files = remove_files ,
6360 substrings_blacklist = substrings_blacklist ,
6461 end_time = timestamp ,
6562 )
@@ -71,14 +68,12 @@ def test_rsyncer_initialises(
7168 assert rsyncer ._server_url == mock_server_url
7269 assert rsyncer ._stop_callback == dummy_callback
7370 assert rsyncer ._local == is_local
74- assert rsyncer ._do_transfer == do_transfer
75- assert rsyncer ._remove_files == remove_files
71+ assert rsyncer ._do_transfer
72+ assert not rsyncer ._remove_files
7673 assert rsyncer ._required_substrings_for_removal == []
7774 assert rsyncer ._substrings_blacklist == substrings_blacklist
7875 assert rsyncer ._notify
7976 assert rsyncer ._end_time == timestamp
80- assert not rsyncer ._finalising
81- assert not rsyncer ._finalised
8277 assert rsyncer ._skipped_files == []
8378 assert (
8479 rsyncer ._remote == str (basepath_remote )
@@ -91,6 +86,145 @@ def test_rsyncer_initialises(
9186 assert isinstance (rsyncer .thread , threading .Thread )
9287 assert not rsyncer ._stopping
9388 assert not rsyncer ._halt_thread
89+ assert not rsyncer ._finalising
90+ assert not rsyncer ._finalised
91+
92+ assert rsyncer .status == "ready"
93+
94+ # Check that it's represented correctly
95+ assert (
96+ str (rsyncer )
97+ == f"<RSyncer ({ rsyncer ._basepath } → { rsyncer ._remote } ) [{ rsyncer .status } ]"
98+ )
99+
100+
101+ @pytest .mark .parametrize ("rsyncer_status" , ("default" , "is_alive" , "stopping" ))
102+ def test_rsyncer_start (
103+ tmp_path : Path ,
104+ mock_server_url : MagicMock ,
105+ rsyncer_status : str ,
106+ ):
107+ # Mock the thread attribute so that it doesn't start an actual Thread
108+ mock_thread = MagicMock ()
109+ mock_thread .start .return_value = None
110+ mock_thread .is_alive .return_value = rsyncer_status == "is_alive"
111+
112+ # Initialise the RSyncer and patch the attributes to be tested
113+ rsyncer = RSyncer (
114+ basepath_local = tmp_path / "local" ,
115+ basepath_remote = tmp_path / "remote" ,
116+ rsync_module = mock .ANY ,
117+ server_url = mock_server_url ,
118+ )
119+ rsyncer .thread = mock_thread
120+ rsyncer ._stopping = rsyncer_status == "stopping"
121+
122+ # Start the RSyncer
123+ if rsyncer_status == "default" :
124+ rsyncer .start ()
125+ mock_thread .start .assert_called_once ()
126+ else :
127+ with pytest .raises (RuntimeError ):
128+ rsyncer .start ()
129+
130+
131+ def test_rsyncer_restart (
132+ mocker : MockerFixture ,
133+ tmp_path : Path ,
134+ mock_server_url : MagicMock ,
135+ ):
136+ # Patch the 'start' class method, which is called by 'restart'
137+ mock_start = mocker .patch .object (RSyncer , "start" )
138+ mock_start .return_value = None
139+
140+ # Mock the thread and the attributes used
141+ mock_thread = MagicMock ()
142+ mock_thread .join .return_value = None
143+
144+ # Initialise the RSyncer and patch the attributes to be tested
145+ rsyncer = RSyncer (
146+ basepath_local = tmp_path / "local" ,
147+ basepath_remote = tmp_path / "remote" ,
148+ rsync_module = mock .ANY ,
149+ server_url = mock_server_url ,
150+ )
151+ rsyncer .thread = mock_thread
152+
153+ # Run 'restart'
154+ rsyncer .restart ()
155+
156+ # Check that the correct calls and attributes are present
157+ mock_thread .join .assert_called_once ()
158+ assert not rsyncer ._halt_thread
159+ assert isinstance (rsyncer .thread , threading .Thread )
160+ mock_start .assert_called_once ()
161+
162+
163+ @pytest .mark .parametrize ("thread_is_alive" , (True , False ))
164+ def test_rsyncer_stop (
165+ tmp_path : Path ,
166+ mock_server_url : MagicMock ,
167+ thread_is_alive : bool ,
168+ ):
169+ # Mock the thread
170+ mock_thread = MagicMock ()
171+ mock_thread .is_alive .return_value = thread_is_alive
172+ mock_thread .join .return_value = None
173+
174+ # Mock the queue
175+ mock_queue = MagicMock ()
176+ mock_queue .join .return_value = None
177+ mock_queue .put .return_value = None
178+
179+ # Initialise the RSyncer and patch the attributes to be tested
180+ rsyncer = RSyncer (
181+ basepath_local = tmp_path / "local" ,
182+ basepath_remote = tmp_path / "remote" ,
183+ rsync_module = mock .ANY ,
184+ server_url = mock_server_url ,
185+ )
186+ rsyncer .thread = mock_thread
187+ rsyncer .queue = mock_queue
188+
189+ # Check that initial attributes are as expected
190+ assert not rsyncer ._stopping
191+ assert not rsyncer ._halt_thread
192+
193+ # Run 'stop' and check that the calls are as expected
194+ rsyncer .stop ()
195+
196+ assert rsyncer ._stopping
197+ assert rsyncer ._halt_thread
198+ if thread_is_alive :
199+ mock_queue .join .assert_called_once ()
200+ mock_queue .put .assert_called_with (None )
201+ mock_thread .join .assert_called_once ()
202+ else :
203+ mock_queue .join .assert_not_called ()
204+ mock_queue .put .assert_not_called ()
205+ mock_thread .join .assert_not_called ()
206+
207+
208+ def test_rsyncer_request_stop (
209+ tmp_path : Path ,
210+ mock_server_url : MagicMock ,
211+ ):
212+ # Initialise the RSyncer
213+ rsyncer = RSyncer (
214+ basepath_local = tmp_path / "local" ,
215+ basepath_remote = tmp_path / "remote" ,
216+ rsync_module = mock .ANY ,
217+ server_url = mock_server_url ,
218+ )
219+
220+ # Check that initial attributes are as expected
221+ assert not rsyncer ._stopping
222+ assert not rsyncer ._halt_thread
223+
224+ # Run 'request_stop' and check that attributes have changed
225+ rsyncer .request_stop ()
226+ assert rsyncer ._stopping
227+ assert rsyncer ._halt_thread
94228
95229
96230@pytest .fixture
@@ -310,3 +444,63 @@ def test_rsyncer_finalise(
310444 # Check that the callback was set at the end
311445 if use_callback :
312446 mock_callback .assert_called_once ()
447+
448+
449+ @pytest .mark .parametrize ("is_stopping" , (True , False ))
450+ def test_rsyncer_enqueue (
451+ tmp_path : Path ,
452+ mock_server_url : MagicMock ,
453+ is_stopping : bool ,
454+ ):
455+ # Mock the queue
456+ mock_queue = MagicMock ()
457+ mock_queue .put .return_value = None
458+
459+ # Initialise the RSyncer and patch the attributes used by the test
460+ rsyncer = RSyncer (
461+ basepath_local = tmp_path / "local" ,
462+ basepath_remote = tmp_path / "remote" ,
463+ rsync_module = mock .ANY ,
464+ server_url = mock_server_url ,
465+ )
466+ rsyncer ._stopping = is_stopping
467+ rsyncer .queue = mock_queue
468+
469+ # Run enqueue with a test file and check that the expected calls were made
470+ rsyncer .enqueue (Path ("test_file" ))
471+ if is_stopping :
472+ mock_queue .put .assert_not_called ()
473+ else :
474+ mock_queue .put .assert_called_once_with (
475+ (tmp_path / "local" / "test_file" ).absolute ()
476+ )
477+
478+
479+ def test_rsyncer_flush_skipped (
480+ tmp_path : Path ,
481+ mock_server_url : MagicMock ,
482+ ):
483+ # Mock the queue
484+ mock_queue = MagicMock ()
485+ mock_queue .put .return_value = None
486+
487+ # Create a list of test files
488+ skipped_files = [
489+ tmp_path / "local" / f"file_{ str (n ).zfill (2 )} .txt" for n in range (20 )
490+ ]
491+
492+ # Initialise the RSyncer and patch the attributes used by the test
493+ rsyncer = RSyncer (
494+ basepath_local = tmp_path / "local" ,
495+ basepath_remote = tmp_path / "remote" ,
496+ rsync_module = mock .ANY ,
497+ server_url = mock_server_url ,
498+ )
499+ rsyncer .queue = mock_queue
500+ rsyncer ._skipped_files = skipped_files
501+
502+ # Run 'flush_skipped' and check that it works as intended
503+ rsyncer .flush_skipped ()
504+ for f in skipped_files :
505+ mock_queue .put .assert_any_call (f )
506+ assert rsyncer ._skipped_files == []
0 commit comments