From dbae50faf78a46d8af5aa21a2e57c34e9a85da5a Mon Sep 17 00:00:00 2001 From: Casey Law Date: Wed, 24 Jun 2026 22:20:35 +0000 Subject: [PATCH] control start_dr will return the name of the beamformed file --- mnc/control.py | 37 ++++++++++++++ tests/test_control.py | 116 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 10 deletions(-) diff --git a/mnc/control.py b/mnc/control.py index 7201dd3..6aba15c 100644 --- a/mnc/control.py +++ b/mnc/control.py @@ -39,6 +39,24 @@ 6: 9800000, 7: 19600000} +_BEAM_RECORDERS = tuple(f'dr{n}' for n in range(1, 11)) + tuple(f'drt{n}' for n in range(1, 3)) + + +def _recording_path_from_response(response): + """Return absolute data file path from a record command response, or None.""" + try: + payload = response['response'] + filename = payload['filename'] + except (KeyError, TypeError): + return None + if os.path.isabs(filename): + return filename + directory = payload.get('directory') + if directory: + return os.path.join(directory, filename) + return filename + + class Controller(): """ Parse configuration and control all subsystems in uniform manner. Ideally, will also make it easy to monitor basic system status. @@ -473,6 +491,13 @@ def start_dr(self, recorders=None, t0='now', duration=None, time_avg=1, teng_f1= teng_f1/2 are the central frequencies of t-engine tunings in units of Hz. f0 sets bandwidth as integer from 1 (250kHz) to 7 (19.6MHz). gain1/2 are t-engine re-quantization gains from 0 (most gain) to 15 (least gain). + + Returns + ------- + dict + Successful beam recordings keyed by recorder name. Each value has + ``status``, ``path``, ``filename``, and ``directory``. Recorders + without a filename are omitted. """ dconf = self.conf['dr'] @@ -500,6 +525,7 @@ def start_dr(self, recorders=None, t0='now', duration=None, time_avg=1, teng_f1= # start ms writing logger.info(f"Starting recorders {recorders} at {start.mjd} (currently {Time.now().mjd})") + recordings = {} for recorder in recorders: # treat encoded recorder name "drt1raw" interpreted as "drt1" with a "raw_record" command. record_command = "raw_record" if "raw" in recorder else "record" @@ -564,12 +590,23 @@ def start_dr(self, recorders=None, t0='now', duration=None, time_avg=1, teng_f1= except (KeyError, TypeError): pass logger.info(f"recording on {recorder}{rec_extra_info}") + if recorder in _BEAM_RECORDERS: + path = _recording_path_from_response(response) + if path is not None: + recordings[recorder] = { + 'status': 'success', + 'path': path, + 'filename': os.path.basename(path), + 'directory': os.path.dirname(path) or '.', + } else: logger.warn(f"recording on {recorder} failed: {response['response']}") if self.drc.read_monitor_point('summary', recorder).value != 'normal': self.drc.read_monitor_point('info', recorder) + return recordings + def status_dr(self, recorders=None): """ Print data recorder info monitor point """ diff --git a/tests/test_control.py b/tests/test_control.py index 0ebc899..8d748a7 100644 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -1,15 +1,111 @@ +import sys import unittest -from unittest.mock import patch -from mnc.control import Controller +from unittest.mock import MagicMock, patch -class TestController(unittest.TestCase): - def setUp(self): - self.controller = Controller() +for _mod in ( + "lwa_f", + "lwa_f.snap2_feng_etcd_client", + "lwa_f.snap2_fengine", + "lwa_f.helpers", + "lwa352_pipeline_control", + "observing", + "observing.obsstate", +): + sys.modules.setdefault(_mod, MagicMock()) - def test_init_with_args(self): - controller = Controller(recorders='dr5') - self.assertEqual(controller.conf['dr']['recorders'], ['dr5']) +from mnc.control import Controller, _recording_path_from_response -if __name__ == '__main__': + +class TestRecordingPathFromResponse(unittest.TestCase): + def test_absolute_filename(self): + response = {"response": {"filename": "/lustre/ubuntu/beam01/D1_123.dat"}} + self.assertEqual( + _recording_path_from_response(response), + "/lustre/ubuntu/beam01/D1_123.dat", + ) + + def test_relative_filename_volt(self): + response = { + "response": { + "filename": "D1_123.dat", + "directory": "/lustre/ubuntu/beam01", + } + } + self.assertEqual( + _recording_path_from_response(response), + "/lustre/ubuntu/beam01/D1_123.dat", + ) + + def test_relative_filename_power(self): + response = { + "response": { + "filename": "D1_123.dat", + "directory": "/lustre/pipeline/beam03", + } + } + self.assertEqual( + _recording_path_from_response(response), + "/lustre/pipeline/beam03/D1_123.dat", + ) + + def test_missing_filename(self): + self.assertIsNone(_recording_path_from_response({"response": {}})) + + +class TestStartDrRecordings(unittest.TestCase): + def test_start_dr_returns_beam_recording_paths(self): + controller = Controller.__new__(Controller) + controller.conf = {"dr": {"recorders": ["dr3"]}} + controller.drvnums = [] + + response = { + "status": "success", + "response": { + "filename": "D1_123.dat", + "directory": "/lustre/pipeline/beam03", + }, + } + controller.drc = MagicMock() + controller.drc.send_command.return_value = (True, response) + summary = MagicMock() + summary.value = "normal" + controller.drc.read_monitor_point.return_value = summary + + recordings = controller.start_dr( + recorders=["dr3"], + duration=60000, + time_avg=100, + ) + + self.assertEqual( + recordings, + { + "dr3": { + "status": "success", + "path": "/lustre/pipeline/beam03/D1_123.dat", + "filename": "D1_123.dat", + "directory": "/lustre/pipeline/beam03", + } + }, + ) + + def test_start_dr_omits_drvs(self): + controller = Controller.__new__(Controller) + controller.conf = {"dr": {"recorders": ["drvs"]}} + controller.drvnums = [] + + response = {"status": "success", "response": {}} + controller.drc = MagicMock() + controller.drc.send_command.return_value = (True, response) + summary = MagicMock() + summary.value = "normal" + controller.drc.read_monitor_point.return_value = summary + + with patch.object(controller, "stop_dr"): + recordings = controller.start_dr(recorders=["drvs"]) + + self.assertEqual(recordings, {}) + + +if __name__ == "__main__": unittest.main() -