Skip to content

Commit 728d0e5

Browse files
authored
Further instrument server fixes + improving test coverage (#570)
* Corrected the logic for deciding on the rsync URL when uploading gain reference. It should look for the rsync URL key first before defaulting to the server URL. * Leave None as-is in JSON data when preparing it in '_start_dc()' * Captured the outputs of the post requests to the 'register_tomo_preproc_params()' and 'register_tomo_proc_params()' endpoints * Added test for the 'upload_gain_reference()' instrument server endpoint and for '_get_murfey_url()'
1 parent aceca04 commit 728d0e5

File tree

5 files changed

+172
-5
lines changed

5 files changed

+172
-5
lines changed

src/murfey/client/multigrid_control.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,15 +403,16 @@ def _start_dc(self, json, from_form: bool = False):
403403
# it is then necessary to extract the data from the message
404404
if from_form:
405405
json = json.get("form", {})
406-
json = {k: str(v) for k, v in json.items()}
406+
# Safely convert all entries into strings, but leave None as-is
407+
json = {k: str(v) if v is not None else None for k, v in json.items()}
407408
self._environment.data_collection_parameters = {
408409
k: None if v == "None" else v for k, v in json.items()
409410
}
410411
source = Path(json["source"])
411412
context = self.analysers[source]._context
412413
if isinstance(context, TomographyContext):
413414
if from_form:
414-
requests.post(
415+
capture_post(
415416
f"{self._environment.url.geturl()}/clients/{self._environment.client_id}/tomography_processing_parameters",
416417
json=json,
417418
)
@@ -442,7 +443,7 @@ def _start_dc(self, json, from_form: bool = False):
442443
)
443444
eer_fractionation_file = eer_response.json()["eer_fractionation_file"]
444445
json.update({"eer_fractionation_file": eer_fractionation_file})
445-
requests.post(
446+
capture_post(
446447
f"{self._environment.url.geturl()}/sessions/{self._environment.murfey_session}/tomography_preprocessing_parameters",
447448
json=json,
448449
)

src/murfey/instrument_server/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,11 @@ def upload_gain_reference(
367367
)
368368

369369
# Return the rsync URL if set, otherwise assume you are syncing via Murfey
370-
rsync_url = urlparse(str(machine_config.get("rsync_url", _get_murfey_url())))
370+
rsync_url = urlparse(
371+
str(machine_config["rsync_url"])
372+
if machine_config.get("rsync_url", "")
373+
else _get_murfey_url()
374+
)
371375
rsync_module = machine_config.get("rsync_module", "data")
372376
rsync_path = f"{rsync_url.hostname}::{rsync_module}/{safe_visit_path}/{safe_destination_dir}/{secure_filename(gain_reference.gain_path.name)}"
373377

src/murfey/util/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ class PreprocessingParametersTomo(BaseModel):
368368
eer_fractionation: int
369369

370370
class Base(BaseModel):
371-
dose_per_frame: float
371+
dose_per_frame: Optional[float]
372372
gain_ref: Optional[str]
373373
manual_tilt_offset: float
374374
eer_fractionation: int

tests/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from configparser import ConfigParser
23

34
import pytest
45
from sqlmodel import Session
@@ -21,6 +22,20 @@ def start_postgres():
2122
murfey_db.commit()
2223

2324

25+
@pytest.fixture()
26+
def mock_client_configuration() -> ConfigParser:
27+
"""
28+
Returns the client-side configuration file as a pre-loaded ConfigParser object.
29+
"""
30+
config = ConfigParser()
31+
config["Murfey"] = {
32+
"instrument_name": "murfey",
33+
"server": "http://0.0.0.0:8000",
34+
"token": "pneumonoultramicroscopicsilicovolcanoconiosis",
35+
}
36+
return config
37+
38+
2439
@pytest.fixture()
2540
def mock_security_configuration(tmp_path):
2641
config_file = tmp_path / mock_security_config_name
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from pathlib import Path
2+
from typing import Optional
3+
from unittest.mock import ANY, Mock, patch
4+
from urllib.parse import urlparse
5+
6+
from pytest import mark
7+
8+
from murfey.instrument_server.api import (
9+
GainReference,
10+
_get_murfey_url,
11+
upload_gain_reference,
12+
)
13+
from murfey.util import posix_path
14+
15+
test_get_murfey_url_params_matrix = (
16+
# Server URL to use
17+
("default",),
18+
("0.0.0.0:8000",),
19+
("murfey_server",),
20+
("http://murfey_server:8000",),
21+
("http://murfey_server:8080/api",),
22+
)
23+
24+
25+
@mark.parametrize("test_params", test_get_murfey_url_params_matrix)
26+
def test_get_murfey_url(
27+
test_params: tuple[str],
28+
mock_client_configuration, # From conftest.py
29+
):
30+
# Unpack test_params
31+
(server_url_to_test,) = test_params
32+
33+
# Replace the server URL from the fixture with other ones for testing
34+
if server_url_to_test != "default":
35+
mock_client_configuration["Murfey"]["server"] = server_url_to_test
36+
37+
# Mock the module-wide config variable with the fixture value
38+
# The fixture is only loaded within the test function, so this patch
39+
# has to happen inside the function instead of as a decorator
40+
with patch("murfey.instrument_server.api.config", mock_client_configuration):
41+
known_server = _get_murfey_url()
42+
43+
# Prepend 'http://' to config URLs that don't have it for the comparison
44+
# Otherwise, urlparse stores it under the 'path' attribute
45+
original_url = str(mock_client_configuration["Murfey"].get("server"))
46+
if not original_url.startswith(("http://", "https://")):
47+
original_url = f"http://{original_url}"
48+
49+
# Check that the components of the result match those in the config
50+
parsed_original = urlparse(original_url)
51+
parsed_server = urlparse(known_server)
52+
assert parsed_server.scheme in ("http", "https")
53+
assert parsed_server.hostname == parsed_original.hostname
54+
assert parsed_server.port == parsed_original.port
55+
assert parsed_server.netloc == parsed_original.netloc
56+
assert parsed_server.path == parsed_original.path
57+
58+
59+
test_upload_gain_reference_params_matrix = (
60+
# Rsync URL settings
61+
("http://1.1.1.1",), # When rsync_url is provided
62+
("",), # When rsync_url is blank
63+
(None,), # When rsync_url not provided
64+
)
65+
66+
67+
@mark.parametrize("test_params", test_upload_gain_reference_params_matrix)
68+
@patch("murfey.instrument_server.api.subprocess")
69+
@patch("murfey.instrument_server.api.tokens")
70+
@patch("murfey.instrument_server.api._get_murfey_url")
71+
@patch("murfey.instrument_server.api.requests")
72+
def test_upload_gain_reference(
73+
mock_request,
74+
mock_get_server_url,
75+
mock_tokens,
76+
mock_subprocess,
77+
test_params: tuple[Optional[str]],
78+
):
79+
80+
# Unpack test parameters and define other ones
81+
(rsync_url_setting,) = test_params
82+
server_url = "http://0.0.0.0:8000"
83+
instrument_name = "murfey"
84+
session_id = 1
85+
86+
# Create a mock machine config base on the test params
87+
rsync_module = "data"
88+
gain_ref_dir = "C:/ProgramData/Gatan/Gain Reference"
89+
mock_machine_config = {
90+
"rsync_module": rsync_module,
91+
"gain_reference_directory": gain_ref_dir,
92+
}
93+
if rsync_url_setting is not None:
94+
mock_machine_config["rsync_url"] = rsync_url_setting
95+
96+
# Assign expected values to the mock objects
97+
mock_response = Mock()
98+
mock_response.status_code = 200
99+
mock_response.json.return_value = mock_machine_config
100+
mock_request.get.return_value = mock_response
101+
mock_get_server_url.return_value = server_url
102+
mock_subprocess.run.return_value = Mock(returncode=0)
103+
104+
# Construct payload and pass request to function
105+
gain_ref_file = f"{gain_ref_dir}/gain.mrc"
106+
visit_path = "2025/aa00000-0"
107+
gain_dest_dir = "processing"
108+
payload = {
109+
"gain_path": gain_ref_file,
110+
"visit_path": visit_path,
111+
"gain_destination_dir": gain_dest_dir,
112+
}
113+
result = upload_gain_reference(
114+
instrument_name=instrument_name,
115+
session_id=session_id,
116+
gain_reference=GainReference(
117+
**payload,
118+
),
119+
)
120+
121+
# Check that the machine config request was called
122+
machine_config_url = f"{server_url}/instruments/{instrument_name}/machine"
123+
mock_request.get.assert_called_once_with(
124+
machine_config_url,
125+
headers={"Authorization": ANY},
126+
)
127+
128+
# Check that the subprocess was run with the expected arguments
129+
# If no rsync_url key is provided, or rsync_url key is empty,
130+
# It should default to the server URL
131+
expected_rsync_url = (
132+
urlparse(server_url) if not rsync_url_setting else urlparse(rsync_url_setting)
133+
)
134+
expected_rsync_path = f"{expected_rsync_url.hostname}::{rsync_module}/{visit_path}/{gain_dest_dir}/gain.mrc"
135+
expected_rsync_cmd = [
136+
"rsync",
137+
posix_path(Path(gain_ref_file)),
138+
expected_rsync_path,
139+
]
140+
mock_subprocess.run.assert_called_once_with(
141+
expected_rsync_cmd,
142+
capture_output=True,
143+
text=True,
144+
)
145+
146+
# Check that the function ran through to completion successfully
147+
assert result == {"success": True}

0 commit comments

Comments
 (0)