Skip to content

Commit ef8e23d

Browse files
committed
Added unit test for the '__init__' and 'finalise' RSyncer class methods
1 parent 011ed4b commit ef8e23d

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

tests/client/test_rsync.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import queue
2+
import threading
3+
from datetime import datetime
4+
from pathlib import Path
5+
from unittest.mock import MagicMock
6+
7+
import pytest
8+
from pytest_mock import MockerFixture
9+
10+
from murfey.client.rsync import RSyncer
11+
from tests.conftest import ExampleVisit
12+
13+
14+
@pytest.fixture
15+
def rsync_module():
16+
return "data"
17+
18+
19+
@pytest.fixture
20+
def mock_server_url():
21+
mock_url = MagicMock()
22+
mock_url.hostname = "10.0.0.1"
23+
return mock_url
24+
25+
26+
# Create a dummy callback function
27+
def dummy_callback():
28+
return None
29+
30+
31+
@pytest.mark.parametrize("is_local", (True, False))
32+
def test_rsyncer_initialises(
33+
tmp_path: Path,
34+
rsync_module: str,
35+
mock_server_url: MagicMock,
36+
is_local: bool,
37+
):
38+
# Assign values to parameters
39+
basepath_local = tmp_path / "local"
40+
basepath_remote = tmp_path / "remote"
41+
do_transfer = True
42+
remove_files = True
43+
44+
# Create a test substrings blacklist dict
45+
substrings_blacklist = {
46+
"directories": ["1", "2", "3"],
47+
"files": ["a", "b", "c"],
48+
}
49+
50+
# Create a timestamp
51+
timestamp = datetime.now()
52+
53+
# Initialise the RSyncer
54+
rsyncer = RSyncer(
55+
basepath_local=basepath_local,
56+
basepath_remote=basepath_remote,
57+
rsync_module=rsync_module,
58+
server_url=mock_server_url,
59+
stop_callback=dummy_callback,
60+
local=is_local,
61+
do_transfer=do_transfer,
62+
remove_files=remove_files,
63+
substrings_blacklist=substrings_blacklist,
64+
end_time=timestamp,
65+
)
66+
67+
# Check that the attributes are as expected
68+
assert rsyncer._basepath == basepath_local.absolute()
69+
assert rsyncer._basepath_remote == basepath_remote
70+
assert rsyncer._rsync_module == rsync_module
71+
assert rsyncer._server_url == mock_server_url
72+
assert rsyncer._stop_callback == dummy_callback
73+
assert rsyncer._local == is_local
74+
assert rsyncer._do_transfer == do_transfer
75+
assert rsyncer._remove_files == remove_files
76+
assert rsyncer._required_substrings_for_removal == []
77+
assert rsyncer._substrings_blacklist == substrings_blacklist
78+
assert rsyncer._notify
79+
assert rsyncer._end_time == timestamp
80+
assert not rsyncer._finalising
81+
assert not rsyncer._finalised
82+
assert rsyncer._skipped_files == []
83+
assert (
84+
rsyncer._remote == str(basepath_remote)
85+
if is_local
86+
else f"{mock_server_url.hostname}::{rsync_module}/{basepath_remote}"
87+
)
88+
assert rsyncer._files_transferred == 0
89+
assert rsyncer._bytes_transferred == 0
90+
assert isinstance(rsyncer.queue, queue.Queue)
91+
assert isinstance(rsyncer.thread, threading.Thread)
92+
assert not rsyncer._stopping
93+
assert not rsyncer._halt_thread
94+
95+
96+
@pytest.fixture
97+
def clem_visit_dir(tmp_path: Path):
98+
visit_name = f"{ExampleVisit.proposal_code}{ExampleVisit.proposal_number}-{ExampleVisit.visit_number}"
99+
visit_dir = tmp_path / "local" / visit_name
100+
visit_dir.mkdir(parents=True, exist_ok=True)
101+
return visit_dir
102+
103+
104+
@pytest.fixture
105+
def clem_test_files(clem_visit_dir: Path):
106+
# Create test files for the DirWatcher to scan
107+
file_list: list[Path] = []
108+
project_dir = clem_visit_dir / "images" / "test_grid"
109+
110+
# Example atlas collection
111+
for s in range(20):
112+
file_list.append(
113+
project_dir
114+
/ "Overview 1"
115+
/ "Image 1"
116+
/ f"Image 1--Stage{str(s).zfill(2)}.tif"
117+
)
118+
file_list.append(
119+
project_dir / "Overview 1" / "Image 1" / "Metadata" / "Image 1.xlif"
120+
)
121+
122+
# Example image stack collection
123+
for c in range(3):
124+
for z in range(10):
125+
file_list.append(
126+
project_dir
127+
/ "TileScan 1"
128+
/ "Position 1"
129+
/ f"Position 1--C{str(c).zfill(2)}--Z{str(z).zfill(2)}.tif"
130+
)
131+
file_list.append(
132+
project_dir / "TileScan 1" / "Position 1" / "Metadata" / "Position 1.xlif"
133+
)
134+
135+
# Create all files and directories specified
136+
for file in file_list:
137+
if not file.parent.exists():
138+
file.parent.mkdir(parents=True)
139+
if not file.exists():
140+
file.touch()
141+
return sorted(file_list)
142+
143+
144+
@pytest.fixture
145+
def clem_junk_files(clem_visit_dir: Path):
146+
# Create junk files that are to be blacklisted from the CLEM workflow
147+
file_list: list[Path] = []
148+
project_dir = clem_visit_dir / "images" / "test_grid"
149+
150+
# Create junk atlas data
151+
for n in range(5):
152+
for s in range(20):
153+
file_list.append(
154+
project_dir
155+
/ "Image 1"
156+
/ f"Image 1_pmd_{n}"
157+
/ f"Image 1_pmd_{n}--Stage{str(s).zfill(2)}.tif"
158+
)
159+
file_list.append(
160+
project_dir / "Image 1" / f"Image 1_pmd_{n}" / "Metadata" / "Image 1.xlif"
161+
)
162+
163+
# Creat junk image data
164+
for n in range(5):
165+
for c in range(3):
166+
for z in range(10):
167+
file_list.append(
168+
project_dir
169+
/ "Position 1"
170+
/ f"Position 1_pmd_{n}"
171+
/ f"Position 1_pmd_{n}--C{str(c).zfill(2)}--Z{str(z).zfill(2)}.tif"
172+
)
173+
file_list.append(
174+
project_dir
175+
/ "Position 1"
176+
/ f"Position 1_pmd_{n}"
177+
/ "Metadata"
178+
/ "Position 1.xlif"
179+
)
180+
181+
# Create remaining junk files
182+
for file_path in (
183+
"1.xlef",
184+
"Metadata/IOManagerConfiguation.xlif",
185+
"Metadata/Overview 1.xlcf",
186+
"Metadata/TileScan 1.xlcf",
187+
"Overview 1/Image 1/Image 1_histo.lof",
188+
"TileScan 1/Position 1/Position 1_histo.lof",
189+
"Overview 1/Image 1/Metadata/Image 1_histo.xlif",
190+
"TileScan 1/Position 1/Metadata/Position 1_histo.xlif",
191+
):
192+
file_list.append(project_dir / file_path)
193+
194+
# Create files and directoriees
195+
for file in file_list:
196+
if not file.parent.exists():
197+
file.parent.mkdir(parents=True)
198+
if not file.exists():
199+
file.touch()
200+
return sorted(file_list)
201+
202+
203+
clem_substrings_blacklist = {
204+
"directories": [
205+
"_pmd_",
206+
],
207+
"files": [
208+
".xlef",
209+
".xlcf",
210+
"_histo.lof",
211+
"_histo.xlif",
212+
"IOManagerConfiguation.xlif",
213+
],
214+
}
215+
216+
rsyncer_finalise_params_matrix: tuple[tuple[str, bool, bool], ...] = (
217+
# Workflow type | Use thread? | Use callback function?
218+
("clem", False, False),
219+
("clem", False, True),
220+
("clem", True, False),
221+
("clem", True, True),
222+
)
223+
224+
225+
@pytest.mark.parametrize("test_params", rsyncer_finalise_params_matrix)
226+
def test_rsyncer_finalise(
227+
mocker: MockerFixture,
228+
rsync_module: str,
229+
mock_server_url: MagicMock,
230+
clem_visit_dir: Path,
231+
clem_test_files: list[Path],
232+
clem_junk_files: list[Path],
233+
test_params: tuple[str, bool, bool],
234+
):
235+
# Unpack test params
236+
workflow_type, use_thread, use_callback = test_params
237+
238+
# Create a test end time
239+
timestamp = datetime.now()
240+
241+
# Mock the class functions/attributes called by the 'finalise' class function
242+
mock_queue = MagicMock()
243+
mock_queue.put.return_value = None
244+
245+
mock_transfer = mocker.patch.object(RSyncer, "_transfer")
246+
mock_transfer.return_value = True
247+
248+
mock_stop = mocker.patch.object(RSyncer, "stop")
249+
mock_stop.return_value = None
250+
251+
mock_process = mocker.patch.object(RSyncer, "_process")
252+
mock_process.return_value = None
253+
254+
mock_callback = MagicMock(return_value=None)
255+
256+
# Initialise the RSyncer class based on the workflow type being tested
257+
if workflow_type == "clem":
258+
rsyncer = RSyncer(
259+
basepath_local=clem_visit_dir / "images",
260+
basepath_remote=Path(clem_visit_dir.name) / "images",
261+
rsync_module=rsync_module,
262+
server_url=mock_server_url,
263+
stop_callback=dummy_callback,
264+
substrings_blacklist=clem_substrings_blacklist,
265+
end_time=timestamp,
266+
)
267+
# Patch the 'queue' attribute with the mocked one
268+
rsyncer.queue = mock_queue
269+
270+
# Check the initial state of attributes that will be changed by 'finalise'
271+
assert not rsyncer._remove_files
272+
assert rsyncer._notify
273+
assert rsyncer._end_time == timestamp
274+
assert not rsyncer._finalising
275+
assert not rsyncer._finalised
276+
277+
# Run the 'finalise' class function with the workflow-specific paths
278+
rsyncer.finalise(
279+
thread=use_thread,
280+
callback=mock_callback if use_callback else None,
281+
)
282+
283+
# Check that attributes are set correctly at the start of the function
284+
assert rsyncer._remove_files
285+
assert not rsyncer._notify
286+
assert rsyncer._end_time is None
287+
assert rsyncer._finalising
288+
289+
# Check that list of files to transfer doesn't include blacklisted files
290+
if use_thread:
291+
for file in clem_test_files:
292+
mock_queue.put.assert_any_call(file)
293+
else:
294+
transfer_args = mock_transfer.call_args.args
295+
assert sorted(transfer_args[0]) == sorted(clem_test_files)
296+
297+
# Check that the blacklisted files no longer exist
298+
for file in clem_junk_files:
299+
assert not file.exists()
300+
# Transfer is being mocked, so check that files to transfer are all present
301+
for file in clem_test_files:
302+
assert file.exists()
303+
304+
# Check that stop was called the correct number of times depending on the setup
305+
assert mock_stop.call_count == 2 if use_thread else 1
306+
307+
# Check that the RSyncer is marked as finalised at the end
308+
assert rsyncer._finalised
309+
310+
# Check that the callback was set at the end
311+
if use_callback:
312+
mock_callback.assert_called_once()

0 commit comments

Comments
 (0)