diff --git a/.gitattributes b/.gitattributes index ebef7bfa1d0b16e37bf47959d2f3624ae1abf80c..ff7a4e0019ed9cfd4bd565b4c0bb9dce133addd8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1966,6 +1966,7 @@ LCU/StationTest/xc_160_verify.sh eol=lf LCU/StationTest/xc_200_setup.sh eol=lf LCU/StationTest/xc_200_verify.sh eol=lf LCU/checkhardware/check_hardware.py -text +LCU/checkhardware/checkhardware_lib/CMakeLists.txt -text LCU/checkhardware/checkhardware_lib/__init__.py -text LCU/checkhardware/checkhardware_lib/data.py -text LCU/checkhardware/checkhardware_lib/db.py -text @@ -1977,6 +1978,7 @@ LCU/checkhardware/checkhardware_lib/lofar.py -text LCU/checkhardware/checkhardware_lib/reporting.py -text LCU/checkhardware/checkhardware_lib/rsp.py -text LCU/checkhardware/checkhardware_lib/settings.py -text +LCU/checkhardware/checkhardware_lib/spectrum_checks/CMakeLists.txt -text LCU/checkhardware/checkhardware_lib/spectrum_checks/__init__.py -text LCU/checkhardware/checkhardware_lib/spectrum_checks/cable_reflection.py -text LCU/checkhardware/checkhardware_lib/spectrum_checks/down.py -text @@ -2046,6 +2048,11 @@ LCU/checkhardware/do_station_test.sh -text svneol=unset#application/x-shellscrip LCU/checkhardware/rtsm.py -text LCU/checkhardware/show_bad_spectra.py -text LCU/checkhardware/show_test_result.py -text +LCU/checkhardware/test/CMakeLists.txt -text +LCU/checkhardware/test/t_check_hardware.py -text +LCU/checkhardware/test/t_check_hardware.run -text +LCU/checkhardware/test/t_check_hardware.sh -text +LCU/checkhardware/test/test-check_hardware.conf -text LCU/checkhardware/update_pvss.py -text LTA/LTAIngest/LTAIngestClient/bin/CMakeLists.txt -text LTA/LTAIngest/LTAIngestClient/bin/ingestaddjobstoqueue -text diff --git a/LCU/checkhardware/CMakeLists.txt b/LCU/checkhardware/CMakeLists.txt index a94754b9dcd79d8ac79b0748e765220cc2d76098..2bddc75a90d43b66a6d55ce8d89da0d463cf4aaf 100644 --- a/LCU/checkhardware/CMakeLists.txt +++ b/LCU/checkhardware/CMakeLists.txt @@ -1,6 +1,15 @@ # $Id$ -lofar_package(checkhardware 1.0) +lofar_package(checkhardware 1.0 DEPENDS PyCommon) +include(PythonInstall) + +# install for testing in cmake +set(_py_files + check_hardware.py +) + +python_install(${_py_files} DESTINATION lofar/lcu/checkhardware) + # Install files matching regex pattern in current directory and below install(DIRECTORY . @@ -14,3 +23,9 @@ install(DIRECTORY config/ USE_SOURCE_PERMISSIONS FILES_MATCHING REGEX "(\\.conf)$" PATTERN ".svn" EXCLUDE) + + +add_subdirectory(test) +add_subdirectory(checkhardware_lib) + + diff --git a/LCU/checkhardware/check_hardware.py b/LCU/checkhardware/check_hardware.py index 9dc57811d14e6ab9b14886dde096c0d544f5bf00..333afef267e223f80d9237de9f207b7f94e252c8 100755 --- a/LCU/checkhardware/check_hardware.py +++ b/LCU/checkhardware/check_hardware.py @@ -51,6 +51,13 @@ from time import sleep import datetime from socket import gethostname import logging +from signal import SIGABRT, SIGINT, SIGTERM, signal +import atexit +from subprocess import Popen, check_call, CalledProcessError, STDOUT, check_output +from functools import partial + +# FIXME: There is _way_ too much going on here outside a function, including things that might fail (like path checks) +# FIXME: emoving hard dependencies on station environment os.umask(001) @@ -283,6 +290,148 @@ def wait_for_start(start_datetime): return +def stop_test_signal(cmd): + logger.info("Stopping test signal.") + + # try to execute command to stop test signal + try: + check_call(cmd, shell=True) + except CalledProcessError as ex: + logger.error(("Could not stop the test signal! Non-zero return code from start_cmd (%s)." % cmd), ex) + raise + +def stop_test_signal_and_exit(cmd, *optargs): + """ + Signal handler that exits with the return code of a passed POSIX signal after executing the provided command. + + :param cmd: The command to stop the test signal + :param optargs: The intercepted POSIX signal + """ + stop_test_signal(cmd) + exit_without_triggering_handler(cmd, *optargs) + + +def exit_without_triggering_handler(cmd, *optargs): + """ + :param cmd: The command to stop the test signal + :param optargs: The intercepted POSIX signal + """ + + # try to get correct return code + logger.info('Now exiting.') + try: + ret_code = int(optargs[0]) # use POSIX signal code + os._exit(ret_code) # sys.exit() won't work here, we don't want to trigger our handler again + # (hm, we could actually call sys.exit and just trigger the atexit handler, but this is more explicit and keeps + # things operational independently.) + except: + os._exit(1) + + +def register_exit_handler(cmd): + """ + execute stop_cmd when script exits normally or with Exception + :param cmd: the command to execute + """ + # execute stop_cmd when script exits normally + atexit.register(stop_test_signal, cmd) + + +def register_signal_handlers(cmd): + """ + execute stop_cmd when script is terminated externally + :param cmd: the command to execute + """ + # execute stop_cmd when script exits normally + atexit.register(stop_test_signal, cmd) + + # handle POSIX signals + for sig in (SIGABRT, SIGINT, SIGTERM): + signal(sig, partial(stop_test_signal_and_exit, cmd)) + + +def start_watchdog_daemon(pid, cmd): + """ + Start a daemon that sits and waits for this script to terminate and then execute the provided command. + We cannot handle SIGKILL / kill -9 from inside the script, so we have to handle that case this way. This may be + a bit --wait for it-- overkill (hah!) and I don't see why this would be needed under normal circumstances, but + nonetheless, since this was requested on the ticket, here we go. + :param cmd: command as shell-executable string + """ + daemon_cmd = 'while ps -p %s > /dev/null; do sleep 1; done; %s' % (pid, cmd) + Popen(daemon_cmd, stdout=open('/dev/null', 'w'), stderr=STDOUT, shell=True, preexec_fn=os.setpgrp) + + +def safely_start_test_signal(start_cmd, stop_cmd): + """ + This will start start_cmd and set things up in a way that stop_cmd is executed in case the check_hardware script + either exits regularly or gets killed for some reason by a POSIX signal. stop_cmd might be executed repeatedly + under circumstances. + :param start_cmd: the command to start as shell-executable string + :param stop_cmd: the command to stop on exit as shell-executable string + """ + + # start signal + try: + check_call(start_cmd, shell=True) + except CalledProcessError as ex: + logger.error("Could not start the test signal! Non-zero return code from start_cmd (%s)." % start_cmd, ex) + raise + + # set things up sp signal is stopped when check_hardware terminates + register_signal_handlers(stop_cmd) + register_exit_handler(stop_cmd) + start_watchdog_daemon(os.getpid(), stop_cmd) # this alone would actually be sufficient + + +def safely_start_test_signal_from_ParameterSet(settings): + ''' + :param settings: A settings.ParameterSet (e.g. obtained through TestSettings.group) + ''' + try: + start_cmd = settings.parset['testsignal']['start-cmd'] + stop_cmd = settings.parset['testsignal']['stop-cmd'] + logger.info('Test signal start/stop settings found. (%s // %s)' % (start_cmd, stop_cmd)) + + # start signal: + safely_start_test_signal(start_cmd, stop_cmd) + + try: + status_cmd = settings.parset['testsignal']['status-cmd'] + ok_status = settings.parset['testsignal']['ok-status'] + logger.info('Test signal status settings found. (%s // %s)' % (status_cmd, ok_status)) + + # wait for signal status to be ok: + wait_for_test_signal_status(status_cmd, ok_status) + + except KeyError: + logger.info('No test signal status settings found.') + + except KeyError: + logger.info('No test signal settings found.') + + +def wait_for_test_signal_status(status_cmd, status, retry_limit=30): + """ + :param status_cmd: command to get test signal status + :param status: the command output to wait for + :param retry_limit: raise RunTimeError after this many status_cmd that did not return status + """ + logger.info("Waiting for '%s' to return '%s'" % (status_cmd, status)) + out = None + for _ in range(retry_limit): + out = check_output(status_cmd, shell=True) + out = out.strip() + if out == status: + logger.info("Status ok.") + return status + else: + logger.info('Wrong status: %s != %s. Try again...'% (out, status)) + time.sleep(1) + + raise RuntimeError("Timed out. Last response was '%s'" % out) + + def main(): global station_name get_arguments() @@ -501,6 +650,7 @@ def main(): lbh.check_rf_power(mode=mode, parset=settings) for mode in (5, 6, 7): + # do all rcumode 5, 6, 7 tests hba = HBA(db) tile_settings = conf.group('rcumode.%d.tile' % mode) @@ -528,6 +678,7 @@ def main(): # if 'RCU%d' % mode in args or 'S%d' % mode in args: if 'S%d' % mode in args: + safely_start_test_signal_from_ParameterSet(tile_settings) hba.check_rf_power(mode=mode, parset=tile_settings) runtime = (time.time() - runstart) @@ -538,12 +689,12 @@ def main(): recordtime = 4 else: recordtime = int(args.get('E%d' % mode)) - + safely_start_test_signal_from_ParameterSet(elem_settings) hba.check_elements(mode=mode, record_time=recordtime, parset=elem_settings) # stop test if driver stopped db.rsp_driver_down = not check_active_rspdriver() - if db.rsp_driver_down and (restarts > 0): + if db.rsp_driver_down and (restarts > 0): # FIXME 'restarts' is undefined at this point?! restarts -= 1 reset_48_volt() time.sleep(30.0) @@ -634,6 +785,7 @@ def main(): return 0 - if __name__ == '__main__': sys.exit(main()) + + diff --git a/LCU/checkhardware/checkhardware_lib/CMakeLists.txt b/LCU/checkhardware/checkhardware_lib/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e5ca32db72ca295e7ab1b50ca18625a046363936 --- /dev/null +++ b/LCU/checkhardware/checkhardware_lib/CMakeLists.txt @@ -0,0 +1,21 @@ +# $Id: CMakeLists.txt $ + +set(_py_files + __init__.py + data.py + db.py + general.py + hardware_tests.py + hba.py + lba.py + lofar.py + reporting.py + rsp.py + settings.py + spu.py + tbb.py +) + +python_install(${_py_files} DESTINATION lofar/lcu/checkhardware/checkhardware_lib) + +add_subdirectory(spectrum_checks) \ No newline at end of file diff --git a/LCU/checkhardware/checkhardware_lib/__init__.py b/LCU/checkhardware/checkhardware_lib/__init__.py index 837db9972b7db5382bdac83aedf7ae111f0a03dc..2a187de83473ab54a4bdf06f0d8c95801b081094 100644 --- a/LCU/checkhardware/checkhardware_lib/__init__.py +++ b/LCU/checkhardware/checkhardware_lib/__init__.py @@ -3,7 +3,7 @@ from general import * from lofar import * -from settings import TestSettings +from settings import TestSettings, ParameterSet from db import DB, db_version from reporting import make_report from spu import SPU diff --git a/LCU/checkhardware/checkhardware_lib/spectrum_checks/CMakeLists.txt b/LCU/checkhardware/checkhardware_lib/spectrum_checks/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..2aaae58c10fc901e76fbb974969cc8e6e7155f17 --- /dev/null +++ b/LCU/checkhardware/checkhardware_lib/spectrum_checks/CMakeLists.txt @@ -0,0 +1,19 @@ +# $Id: CMakeLists.txt $ + +set(_py_files + __init__.py + cable_reflections.py + down.py + down_old.py + flat.py + noise.py + oscillation.py + peakslib.py + rf_power.py + short.py + spurious.py + summator_noise.py + tools.py +) + +python_install(${_py_files} DESTINATION lofar/lcu/checkhardware/checkhardware_lib/spectrum_checks) diff --git a/LCU/checkhardware/config/FR606-check_hardware.conf b/LCU/checkhardware/config/FR606-check_hardware.conf index f72e209b6823125c736aa0d727c6712495c40913..045a901b766ecbdbec7488484f28d86e559aa2fb 100755 --- a/LCU/checkhardware/config/FR606-check_hardware.conf +++ b/LCU/checkhardware/config/FR606-check_hardware.conf @@ -29,8 +29,8 @@ station= FR606C always= RV, TV, RBC, TBC list.0= list.1= SPU,TM,RCU3,RCU5 -list.2= SPU,TM,RCU3,M5,SN5,O5,N5,SP5,S7,E7 -list.3= S3 +list.2= SPU,TM,RCU3,M5,SN5,O5,N5,SP5,S5,E5 +list.3= S5,E5 [spu] temperature.min= 10.0 @@ -131,10 +131,10 @@ short.mean-pwr.min= 55.0 short.mean-pwr.max= 61.0 flat.mean-pwr.min= 61.0 flat.mean-pwr.max= 64.5 -rf.subbands= 105 +rf.subbands= 256 rf.min-sb-pwr= 65.0 -rf.negative-deviation= -24.0 -rf.positive-deviation= 12.0 +rf.negative-deviation= -28.0 +rf.positive-deviation= 16.0 noise.negative-deviation= -3.0 noise.positive-deviation= 1.5 noise.max-difference= 1.5 @@ -147,12 +147,16 @@ oscillation.min-peak-pwr= 6.0 oscillation.passband= 1:511 spurious.min-peak-pwr= 3.0 spurious.passband= 1:511 +testsignal.start-cmd= echo set_config 150.0 -10 | nc ncu 8093 +testsignal.stop-cmd= echo bye | nc ncu 8093 +testsignal.status-cmd= echo get_config | nc ncu 8093 | grep Frequency +testsignal.ok-status= Frequency: 150 MHz Power level: -10 dBm RF: ON [rcumode.5.element] -rf.subbands= 105 +rf.subbands= 256 rf.min-sb-pwr= 65.0 -rf.negative-deviation= -24.0 -rf.positive-deviation= 12.0 +rf.negative-deviation= -30.0 +rf.positive-deviation= 16.0 noise.negative-deviation= -3.0 noise.positive-deviation= 1.5 noise.max-difference= 1.5 @@ -161,6 +165,10 @@ oscillation.min-peak-pwr= 6.0 oscillation.passband= 1:511 spurious.min-peak-pwr= 3.0 spurious.passband= 1:511 +testsignal.start-cmd= echo set_config 150.0 0 | nc ncu 8093 +testsignal.stop-cmd= echo bye | nc ncu 8093 +testsignal.status-cmd= echo get_config | nc ncu 8093 | grep Frequency +testsignal.ok-status= Frequency: 150 MHz Power level: 0 dBm RF: ON [rcumode.6.tile] short.mean-pwr.min= 55.0 diff --git a/LCU/checkhardware/test/CMakeLists.txt b/LCU/checkhardware/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..acc3d69163f87e951e5c5ec2da0e9e7405a45bcb --- /dev/null +++ b/LCU/checkhardware/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# $Id: CMakeLists.txt 33404 2016-01-27 10:21:14Z jkuensem $ + +include(LofarCTest) + +lofar_add_test(t_check_hardware) diff --git a/LCU/checkhardware/test/t_check_hardware.py b/LCU/checkhardware/test/t_check_hardware.py new file mode 100644 index 0000000000000000000000000000000000000000..54e5d8c78d1d8b0d5824826cb44200b0f929e1bf --- /dev/null +++ b/LCU/checkhardware/test/t_check_hardware.py @@ -0,0 +1,429 @@ +import unittest +import os +from mock import MagicMock, patch, call +import sys +import logging +import subprocess +import signal +import atexit +import time + +logger = logging.getLogger(__name__) + +# mock out check for existing log directory on script import as module +os.access = MagicMock(return_value=True) + +# mock out modules with relative imports (that only work with the namespace when executed as a script) +# FIXME: make sure that absolute imports are ok and don't break things in production. +# FIXME: ...Then fix the implementation and remove this mock so we can test those modules. +with patch.dict('sys.modules', **{ + 'checkhardware_lib': MagicMock(), + 'checkhardware_lib.data': MagicMock(), + 'checkhardware_lib.rsp': MagicMock(), + 'cable_reflection': MagicMock(), + 'logging': MagicMock(), +}): + # import these here so we can before mock out checks on station environment + import lofar.lcu.checkhardware.check_hardware as check_hardware + from lofar.lcu.checkhardware.checkhardware_lib import TestSettings, ParameterSet + check_hardware.logger = logger # override logger to handle logging output here + + +class TestCheckHardware(unittest.TestCase): + + def setUp(self): + logger.info(">>>---> %s <---<<<" % self._testMethodName) + + # mock exit call to not actually exit the test + os._exit = MagicMock() + # we don't want to actually call anything + check_hardware.check_call = MagicMock() + + self.elem_settings_no_testsignal = ParameterSet() + self.elem_settings_no_testsignal.parset = {'spurious': {'min-peak-pwr': '3.0', + 'passband': '1:511'}, + 'rf': {'negative-deviation': '-24.0', + 'subbands': '105', + 'min-sb-pwr': '65.0', + 'positive-deviation': '12.0'}, + 'noise': {'negative-deviation': '-3.0', + 'max-difference': '1.5', + 'positive-deviation': '1.5', + 'passband': '1:511'}, + 'oscillation': {'min-peak-pwr': '6.0', + 'passband': '1:511'}} + + self.elem_settings_testsignal = ParameterSet() + self.elem_settings_testsignal.parset = {'spurious': {'min-peak-pwr': '3.0', + 'passband': '1:511'}, + 'rf': {'negative-deviation': '-24.0', + 'subbands': '105', + 'min-sb-pwr': '65.0', + 'positive-deviation': '12.0'}, + 'noise': {'negative-deviation': '-3.0', + 'max-difference': '1.5', + 'positive-deviation': '1.5', + 'passband': '1:511'}, + 'testsignal': {'start-cmd': 'echo set config 56.0 -10 | nc ncu 8093', + 'stop-cmd': 'echo bye | nc ncu 8093'}, + 'oscillation': {'min-peak-pwr': '6.0', + 'passband': '1:511'}} + + self.elem_settings_testsignal_with_status = ParameterSet() + self.elem_settings_testsignal_with_status.parset = {'spurious': {'min-peak-pwr': '3.0', + 'passband': '1:511'}, + 'rf': {'negative-deviation': '-24.0', + 'subbands': '105', + 'min-sb-pwr': '65.0', + 'positive-deviation': '12.0'}, + 'noise': {'negative-deviation': '-3.0', + 'max-difference': '1.5', + 'positive-deviation': '1.5', + 'passband': '1:511'}, + 'testsignal': {'start-cmd': 'echo set config 56.0 -10 | nc ncu 8093', + 'stop-cmd': 'echo bye | nc ncu 8093', + 'status-cmd': "echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON'", + 'ok-status': "Frequency: 56 MHz Power level: -10 dBm RF: ON"}, + 'oscillation': {'min-peak-pwr': '6.0', + 'passband': '1:511'}} + + + def test_safely_start_test_signal(self): + """ Verify that the provided command is executed and handlers are registered correctly""" + + # test value + start_cmd = 'echo "Start the signal!"' + stop_cmd = 'echo "Stop the signal!"' + + # setup test + with patch.object(check_hardware, 'register_exit_handler'), \ + patch.object(check_hardware, 'register_signal_handlers'), \ + patch.object(check_hardware, 'start_watchdog_daemon'): + + # trigger action + check_hardware.safely_start_test_signal(start_cmd, stop_cmd) + + # assert correct behavior + check_hardware.check_call.assert_called_with(start_cmd, shell=True) + check_hardware.register_exit_handler.assert_called_with(stop_cmd) + check_hardware.register_signal_handlers.assert_called_with(stop_cmd) + check_hardware.start_watchdog_daemon.assert_called_with(os.getpid(), stop_cmd) + + def test_safely_start_test_signal_logs_and_reraises_CalledProcessError(self): + """ Verify that the provided command is executed and handlers are registered correctly""" + + # test value + start_cmd = 'echo "Start the signal!"' + stop_cmd = 'echo "Stop the signal!"' + + # setup test + with patch.object(check_hardware, 'register_exit_handler'), \ + patch.object(check_hardware, 'register_signal_handlers'), \ + patch.object(check_hardware, 'start_watchdog_daemon'), \ + patch.object(check_hardware, 'check_call', MagicMock(side_effect=subprocess.CalledProcessError('', ''))), \ + patch.object(check_hardware.logger, 'error'): + + with self.assertRaises(subprocess.CalledProcessError): + # trigger action + check_hardware.safely_start_test_signal(start_cmd, stop_cmd) + + # assert correct behavior + check_hardware.logger.error.assert_called() + + def test_safely_start_test_signal_from_ParameterSet_turns_signal_and_waits_for_status_correctly(self): + """ Verify that the commands from ParameterSet are passed on to safely_start_test_signal and wait_for_test_signal_status""" + + # test value + start_cmd = 'echo set config 56.0 -10 | nc ncu 8093' + stop_cmd = 'echo bye | nc ncu 8093' + expected_status_cmd = "echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON'" + expected_ok_status = "Frequency: 56 MHz Power level: -10 dBm RF: ON" + + # setup test + with patch.object(check_hardware, 'safely_start_test_signal'), \ + patch.object(check_hardware, 'wait_for_test_signal_status'): + + # trigger action + check_hardware.safely_start_test_signal_from_ParameterSet(self.elem_settings_testsignal_with_status) + + # assert correct behavior + check_hardware.safely_start_test_signal.assert_called_with(start_cmd, stop_cmd) + check_hardware.wait_for_test_signal_status.assert_called_with(expected_status_cmd, expected_ok_status) + + def test_safely_start_test_signal_from_ParameterSet_does_nothing_when_no_stationsignal_keys_in_ParameterSet(self): + """ Verify that the commands from ParameterSet are passed on to safely_start_test_signal and wait_for_test_signal_status is not called""" + + # setup test + with patch.object(check_hardware, 'safely_start_test_signal'), \ + patch.object(check_hardware, 'wait_for_test_signal_status'): + + # trigger action + check_hardware.safely_start_test_signal_from_ParameterSet(self.elem_settings_no_testsignal) + + # assert correct behavior + check_hardware.safely_start_test_signal.assert_not_called() + check_hardware.wait_for_test_signal_status.assert_not_called() + + def test_safely_start_test_signal_from_ParameterSet_only_starts_signal_when_no_status_keys_in_ParameterSet(self): + """ Verify that safely_start_test_signal and wait_for_test_signal_status are not called""" + + # test value + start_cmd = 'echo set config 56.0 -10 | nc ncu 8093' + stop_cmd = 'echo bye | nc ncu 8093' + + # setup test + with patch.object(check_hardware, 'safely_start_test_signal'), \ + patch.object(check_hardware, 'wait_for_test_signal_status'): + # trigger action + check_hardware.safely_start_test_signal_from_ParameterSet(self.elem_settings_testsignal) + + # assert correct behavior + check_hardware.safely_start_test_signal.assert_called_with(start_cmd, stop_cmd) + check_hardware.wait_for_test_signal_status.assert_not_called() + + def test_stop_test_signal(self): + """ Verify that the provided command is executed """ + + # test value + cmd = 'echo "Stop the signal! 1"' + + # trigger action + check_hardware.stop_test_signal(cmd) + + # assert correct behavior + os._exit.assert_not_called() + check_hardware.check_call.assert_called_with(cmd, shell=True) # command is executed + + def test_stop_test_signal_and_exit_defaults_to_code_1(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # test value + cmd = 'echo "Stop the signal! 2"' + + # trigger action + check_hardware.stop_test_signal_and_exit(cmd) + + # assert correct behavior + os._exit.assert_called_with(1) # exit code correct + check_hardware.check_call.assert_called_with(cmd, shell=True) # command is executed + + def test_stop_test_signal_and_exit_handles_signal_correctly(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # test value + cmd = 'echo "Stop the signal! 2"' + signal_code = 42 + + # trigger action + check_hardware.stop_test_signal_and_exit(cmd, signal_code, KeyboardInterrupt()) + + # assert correct behavior + os._exit.assert_called_with(signal_code) # exit code correct + check_hardware.check_call.assert_called_with(cmd, shell=True) # command is executed + + def test_wait_for_test_signal_status_waits_for_correct_status(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # test value + status_cmd = 'mockme' + responses = ['ne', 'ja\n', 'ne'] + waitfor = 'ja' + + with patch.object(check_hardware, 'check_output', MagicMock(side_effect=responses)),\ + patch('time.sleep'): + + # trigger action + check_hardware.wait_for_test_signal_status(status_cmd, waitfor) + + # assert correct behavior + check_hardware.check_output.called_with(status_cmd, shell=True) # command is executed + self.assertEqual(check_hardware.check_output.call_count, 2) + + def test_wait_for_test_signal_status_raises_RuntimeError_when_retry_limit_reached(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # test value + limit=15 + status_cmd = 'mockme' + responses = ['ne'] * limit # only 30 are read + responses.append('ja') + waitfor = 'ja' + + with patch.object(check_hardware, 'check_output', MagicMock(side_effect=responses)),\ + patch('time.sleep'): + + # trigger action + + with self.assertRaises(RuntimeError): + check_hardware.wait_for_test_signal_status(status_cmd, waitfor, retry_limit=limit) + + # assert correct behavior + check_hardware.check_output.called_with(status_cmd, shell=True) # command is executed + self.assertEqual(check_hardware.check_output.call_count, limit) + + + def test_register_signal_handlers_stops_test_signal_on_POSIX_signal(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # test value + cmd = 'echo "Stop the signal! 3"' + + # register handlers we want to test + check_hardware.register_signal_handlers(cmd) + + # trigger action: + pid = os.getpid() + os.kill(pid, signal.SIGINT) # code 2 + os.kill(pid, signal.SIGABRT) # code 6 + os.kill(pid, signal.SIGTERM) # code 15 + + # assert correct behavior + os._exit.assert_has_calls([call(2), call(6), call(15)]) # all signal error codes correct + check_hardware.check_call.assert_called_with(cmd, shell=True) # command is executed + + def test_register_exit_handler_stops_test_signal_on_normal_exit(self): + """ Verify that the provided command is executed and os._exit is called with correct return code """ + + # This test turned out nastier than expected. + # The problem is that we cannot catch the SystemExit within the test, because the atexit hooks only fire after + # the test exits (even after tearDownClass), so we will get a stacktrace printed, but cmake won't count the + # assert failures as failure of the test. + # Note: As long as we use the watchdog, this is redundant anyway and we could also change the implementation + # to explicitely turn the test signal off before it exits and test for that instead. + # But who wants the easy way out, right? ;) + # FIXME: Find a way to make sure this test fails if the assertion fails or find a smarter way to test this. + + # test value + cmd = 'echo "Stop the signal! 4"' + + # assert correct behavior + def assert_on_exit(): + logger.info('>>>----> Asserting on exit!') + check_hardware.check_call.assert_called_with(cmd, shell=True) # command is executed + + # register a handler to trigger the assert. + atexit.register(assert_on_exit) + + # register handlers we want to test + check_hardware.register_exit_handler(cmd) + + # The test will now regularly exit with code 0, hopefully triggering all these hooks + + + def test_start_watchdog_daemon_stops_test_signal_when_provided_pid_is_killed(self): + """ Verify that the provided command is executed when watched process dies """ + + tmpfile = "/tmp/t_checkhardware.%s" % time.time() + + # test value + good_message = 'Stop the signal! 5' + cmd = 'echo "%s" > %s' % (good_message, tmpfile) + + # start dummy process + p = subprocess.Popen(['sleep', '120']) + + # start watchdog for dummy process + check_hardware.start_watchdog_daemon(p.pid, cmd) + + # kill dummy + os.kill(p.pid, signal.SIGKILL) + os.wait() + + # check temporary file to confirm the watchdog command has been executed + for i in range (30): + if os.path.isfile(tmpfile): + break + time.sleep(1) + self.assertTrue(os.path.isfile(tmpfile)) + with open(tmpfile) as f: + lines = f.read().split('\n') + self.assertTrue(good_message in lines) # cmd has been executed + + os.remove(tmpfile) + + + # FIXME: Move this to t_settings once that exists + def test_settings_parset_raises_KeyError_when_accessing_missing_key(self): + + # assert KeyError if setting not there + with self.assertRaises(KeyError): + logger.info(self.elem_settings_no_testsignal.parset['testsignal']['status-cmd']) + + + # FIXME: Move this to t_settings once that exists + def test_settings_contains_testsignal_commands_from_config_file(self): + + # test_values + expected_start_cmd = "echo set config 56.0 -10 | nc ncu 8093" + expected_stop_cmd = "echo bye | nc ncu 8093" + expected_status_cmd = "echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON'" + expected_ok_status = "Frequency: 56 MHz Power level: -10 dBm RF: ON" + + # read settings + f = os.environ.get('srcdir')+'/test-check_hardware.conf' + settings = TestSettings(filename=f) + elem_settings = settings.group('rcumode.5.element') + start_cmd = elem_settings.parset['testsignal']['start-cmd'] + stop_cmd = elem_settings.parset['testsignal']['stop-cmd'] + status_cmd = elem_settings.parset['testsignal']['status-cmd'] + ok_status = elem_settings.parset['testsignal']['ok-status'] + + # assert correct values + self.assertEqual(start_cmd, expected_start_cmd) + self.assertEqual(stop_cmd, expected_stop_cmd) + self.assertEqual(status_cmd, expected_status_cmd) + self.assertEqual(ok_status, expected_ok_status) + + #@unittest.skip('disabled due to fork bomb behavior') + def test_main_turns_signal_with_commands_from_settings(self): + + # test values + expected_start_cmd = "echo set config 56.0 -10 | nc ncu 8093" + expected_stop_cmd = "echo bye | nc ncu 8093" + expected_status_cmd = "echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON'" + expected_ok_status = "Frequency: 56 MHz Power level: -10 dBm RF: ON" + + # setup tests + # todo: mock the ParameterSet instead, once the imports are resolved and this can be done straight-forward + check_hardware.conf_file = r'test-check_hardware.conf' + check_hardware.confpath = os.environ.get('srcdir')+'/' + + # pretend to be a station + # FIXME: correct behavior of mocked-out parts should be covered by additional tests + # FIXME: why is all this actually necessary when I only run an element test? + with patch.object(check_hardware, 'read_station_config', MagicMock(return_value=(1, 1, 1, 1, 1, 1, 1))), \ + patch.object(check_hardware, 'safely_start_test_signal'), \ + patch.object(check_hardware, 'wait_for_test_signal_status'), \ + patch.object(check_hardware, 'swlevel', MagicMock(return_value=(5, None))), \ + patch.object(check_hardware, 'rspctl'), \ + patch.object(check_hardware, 'RSP'), \ + patch.object(check_hardware, 'check_active_boards', MagicMock(return_value=(1, 1))), \ + patch.object(check_hardware, 'check_active_tbbdriver', MagicMock(return_value=True)), \ + patch.object(check_hardware, 'check_active_rspdriver', MagicMock(return_value=True)), \ + patch.object(check_hardware, 'reset_rsp_settings'), \ + patch.object(check_hardware, 'HBA'), \ + patch.object(check_hardware, 'reset_48_volt'), \ + patch.object(check_hardware, 'tbbctl'), \ + patch.object(os, 'listdir'), \ + patch.object(os, 'remove'): # I'm scared... + + # patch arguments: pretend script was started with these. + # -TST (test mode) + # -e5: (element test in mode 5) + # Names optimized for disk space + testargs = ["check_hardware.py", '-TST', '-e5', '-s5'] + with patch.object(sys, 'argv', testargs): + # trigger action + check_hardware.main() # Warning: Something acts as a fork bomb when mocks are not setup properly! + + check_hardware.safely_start_test_signal.assert_called_with(expected_start_cmd, expected_stop_cmd) + check_hardware.wait_for_test_signal_status.assert_called_with(expected_status_cmd, expected_ok_status) + self.assertEqual(check_hardware.safely_start_test_signal.call_count, 2) + self.assertEqual(check_hardware.wait_for_test_signal_status.call_count, 2) + + +if __name__ == "__main__": + logger.level = logging.DEBUG + stream_handler = logging.StreamHandler(sys.stdout) + logger.addHandler(stream_handler) + unittest.main() diff --git a/LCU/checkhardware/test/t_check_hardware.run b/LCU/checkhardware/test/t_check_hardware.run new file mode 100755 index 0000000000000000000000000000000000000000..3a348cbba93a2ac84832d7c0579197fa21577f4e --- /dev/null +++ b/LCU/checkhardware/test/t_check_hardware.run @@ -0,0 +1,5 @@ +#!/bin/bash + +# Run the unit test +source python-coverage.sh +python_coverage_test "checkhardware*" t_check_hardware.py diff --git a/LCU/checkhardware/test/t_check_hardware.sh b/LCU/checkhardware/test/t_check_hardware.sh new file mode 100755 index 0000000000000000000000000000000000000000..43193a4253494dc8d06e0dac425a066d4a5f80b4 --- /dev/null +++ b/LCU/checkhardware/test/t_check_hardware.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./runctest.sh t_check_hardware diff --git a/LCU/checkhardware/test/test-check_hardware.conf b/LCU/checkhardware/test/test-check_hardware.conf new file mode 100644 index 0000000000000000000000000000000000000000..4b7c43ad06d8ed9d560c4c77dc641de6a4a2c085 --- /dev/null +++ b/LCU/checkhardware/test/test-check_hardware.conf @@ -0,0 +1,255 @@ +# +# configuration file for check_hardware.py +# + +[configuration] +version= 00.01 +station= FR606C + +# checks to do if '-l=x' argument is given, all checks behind list.x are executed +# always checks will be done always +# +# S(rcumode) : signal check for rcumode (also down and flat-check in rcumode 1..4). +# O(rcumode) : oscillation check for rcumode. +# SP(rcumode) : spurious check for rcumode. +# N(rcumode)[= 300]: noise check for rcumode, optional data time in seconds +# default data time= 120 sec. +# E(rcumode)[= 60] : do all RCU5 element tests, optional data time in seconds. +# default data time= 10 sec. +# M(rcumode) : do modem +# SN(rcumode) : do summator noise +# +# RCU(mode) : do all rcu checks for given mode, no element tests done. +# +# RBC : RSP voltage/temperature check +# TBC : TBB voltage/temperature check +# SPU : SPU voltage +# TM : TBB memmory +[check] +always= +list.0= +list.1= SPU,TM,RCU3,RCU5 +list.2= SPU,TM,RCU3,M5,SN5,O5,N5,SP5,S7,E7 +list.3= S3 + +[spu] +temperature.min= 10.0 +temperature.max= 35.0 +voltage.3_3.min= 3.1 +voltage.3_3.max= 3.4 +voltage.3_3.max-drop= 0.3 +voltage.5_0.min= 4.5 +voltage.5_0.max= 5.0 +voltage.5_0.max-drop= 0.3 +voltage.8_0.min= 7.4 +voltage.8_0.max= 8.0 +voltage.8_0.max-drop= 0.3 +voltage.48_0.min= 43.0 +voltage.48_0.max= 48.0 +voltage.48_0.max-drop= 2.0 + +[tbb] +version.tp= 2.4 +version.mp= 3.0 +temperature.min= 10.0 +temperature.max= 45.0 +temperature.tp.min= 10.0 +temperature.tp.max= 75.0 +temperature.tp.max_delta= 10.0 +temperature.mp.min= 10.0 +temperature.mp.max= 75.0 +temperature.mp.max_delta= 10.0 +voltage.1_2.min= 1.1 +voltage.1_2.max= 1.3 +voltage.2_5.min= 2.4 +voltage.2_5.max= 2.6 +voltage.3_3.min= 3.1 +voltage.3_3.max= 3.4 + +[rsp] +version.ap= 8.2 +version.bp= 8.2 +temperature.min= 10.0 +temperature.max= 50.0 +temperature.ap.min= 10.0 +temperature.ap.max= 80.0 +temperature.ap.max_delta= 10.0 +temperature.bp.min= 10.0 +temperature.bp.max= 80.0 +temperature.bp.max_delta= 10.0 +voltage.1_2.min= 1.1 +voltage.1_2.max= 1.3 +voltage.2_5.min= 2.4 +voltage.2_5.max= 2.6 +voltage.3_3.min= 3.1 +voltage.3_3.max= 3.4 + +[rcumode.1-3] +short.mean-pwr.min= 55.0 +short.mean-pwr.max= 61.0 +flat.mean-pwr.min= 61.0 +flat.mean-pwr.max= 64.5 +rf.subbands= 301 +rf.min-sb-pwr= 75.0 +rf.negative-deviation= -3.0 +rf.positive-deviation= 3.0 +noise.negative-deviation= -2.5 +noise.positive-deviation= 2.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +cable-reflection.min-peak-pwr= 0.8 +cable-reflection.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 +down.passband= 231:371 + +[rcumode.2-4] +short.mean-pwr.min= 55.0 +short.mean-pwr.max= 61.0 +flat.mean-pwr.min= 61.0 +flat.mean-pwr.max= 64.5 +rf.subbands= 301 +rf.min-sb-pwr= 75.0 +rf.negative-deviation= -3.0 +rf.positive-deviation= 3.0 +noise.negative-deviation= -2.5 +noise.positive-deviation= 2.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +cable-reflection.min-peak-pwr= 0.8 +cable-reflection.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 +down.passband= 231:371 + +[rcumode.5.tile] +short.mean-pwr.min= 55.0 +short.mean-pwr.max= 61.0 +flat.mean-pwr.min= 61.0 +flat.mean-pwr.max= 64.5 +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 1.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +summator-noise.min-peak-pwr= 1.2 +summator-noise.passband= 45:135,200:270 +cable-reflection.min-peak-pwr= 0.8 +cable-reflection.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 +testsignal.start-cmd= echo set config 56.0 -10 | nc ncu 8093 +testsignal.stop-cmd= echo bye | nc ncu 8093 +testsignal.status-cmd= echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON' +testsignal.ok-status= Frequency: 56 MHz Power level: -10 dBm RF: ON + +[rcumode.5.element] +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 1.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 +testsignal.start-cmd= echo set config 56.0 -10 | nc ncu 8093 +testsignal.stop-cmd= echo bye | nc ncu 8093 +testsignal.status-cmd= echo 'Frequency: 56 MHz Power level: -10 dBm RF: ON' +testsignal.ok-status= Frequency: 56 MHz Power level: -10 dBm RF: ON + +[rcumode.6.tile] +short.mean-pwr.min= 55.0 +short.mean-pwr.max= 61.0 +flat.mean-pwr.min= 61.0 +flat.mean-pwr.max= 64.5 +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 1.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +summator-noise.min-peak-pwr= 1.2 +summator-noise.passband= 45:135 +cable-reflection.min-peak-pwr= 0.8 +cable-reflection.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 + +[rcumode.6.element] +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 1.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 +testsignal.start-cmd= echo set config 127.5 0 | nc ncu 8093 +testsignal.stop-cmd= echo bye | nc ncu 8093 + +[rcumode.7.tile] +short.mean-pwr.min= 55.0 +short.mean-pwr.max= 61.0 +flat.mean-pwr.min= 61.0 +flat.mean-pwr.max= 64.5 +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 1.5 +noise.max-difference= 1.5 +noise.passband= 1:511 +summator-noise.min-peak-pwr= 1.2 +summator-noise.passband= 45:135 +cable-reflection.min-peak-pwr= 0.8 +cable-reflection.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 + +[rcumode.7.element] +rf.subbands= 105 +rf.min-sb-pwr= 65.0 +rf.negative-deviation= -24.0 +rf.positive-deviation= 12.0 +noise.negative-deviation= -3.0 +noise.positive-deviation= 3.0 +noise.max-difference= 1.5 +noise.passband= 1:511 +oscillation.min-peak-pwr= 6.0 +oscillation.passband= 1:511 +spurious.min-peak-pwr= 3.0 +spurious.passband= 1:511 + +# General settings +[paths] +global-data= /globalhome/log/stationtest +local-data= /opt/stationtest/data +local-report-dir= /localhome/stationtest/data +global-report-dir= /globalhome/log/stationtest + +[files] +bad-antenna-list= /localhome/stationtest/data/bad_antenna_list.txt