update to gerrit version
Change-Id: Ifdaa28dd961a529cd9197c4c3639744f108b0a6a
This commit is contained in:
parent
ff6a98af92
commit
aa7910c28c
@ -31,6 +31,7 @@ sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
||||
|
||||
import logging
|
||||
from mlzlog import ColoredConsoleHandler
|
||||
|
||||
from frappy.gui.qt import QApplication
|
||||
from frappy.gui.cfg_editor.mainwindow import MainWindow
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# pylint: disable=invalid-name
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
@ -23,8 +22,8 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import sys
|
||||
from os import path
|
||||
|
||||
# Add import path for inplace usage
|
||||
@ -61,8 +60,9 @@ def parseArgv(argv):
|
||||
action='store',
|
||||
help="comma separated list of cfg files,\n"
|
||||
"defaults to <name_of_the_instance>.\n"
|
||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
||||
"else they are treated as path names",
|
||||
"cfgfiles given without '.cfg' extension are searched"
|
||||
" in the configuration directory,"
|
||||
" else they are treated as path names",
|
||||
default=None)
|
||||
parser.add_argument('-g',
|
||||
'--gencfg',
|
||||
@ -96,15 +96,13 @@ def main(argv=None):
|
||||
generalConfig.init(args.gencfg)
|
||||
logger.init(loglevel)
|
||||
|
||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles,
|
||||
interface=args.port, testonly=args.test)
|
||||
|
||||
if args.daemonize:
|
||||
srv.start()
|
||||
else:
|
||||
try:
|
||||
srv.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
srv.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
43
cfg/seop_cfg.py
Normal file
43
cfg/seop_cfg.py
Normal file
@ -0,0 +1,43 @@
|
||||
description = """
|
||||
3He system in Lab ...
|
||||
"""
|
||||
Node('mlz_seop',
|
||||
description,
|
||||
'tcp://10767',
|
||||
)
|
||||
|
||||
Mod('cell',
|
||||
'frappy_mlz.seop.Cell',
|
||||
'interface module to the driver',
|
||||
config_directory = '/home/jcns/daemon/config',
|
||||
)
|
||||
|
||||
Mod('afp',
|
||||
'frappy_mlz.seop.Afp',
|
||||
'controls the afp flip of the cell',
|
||||
cell = 'cell'
|
||||
)
|
||||
|
||||
Mod('nmr',
|
||||
'frappy_mlz.seop.Nmr',
|
||||
'controls the ',
|
||||
cell = 'cell'
|
||||
)
|
||||
|
||||
fitparams = [
|
||||
('amplitude', 'V'),
|
||||
('T1', 's'),
|
||||
('T2', 's'),
|
||||
('b', ''),
|
||||
('frequency', 'Hz'),
|
||||
('phase', 'deg'),
|
||||
]
|
||||
for param, unit in fitparams:
|
||||
Mod(f'nmr_{param.lower()}',
|
||||
'frappy_mlz.seop.FitParam',
|
||||
f'fittet parameter {param} of NMR',
|
||||
cell = 'cell',
|
||||
value = Param(unit=unit),
|
||||
sigma = Param(unit=unit),
|
||||
param = param,
|
||||
)
|
46
debian/changelog
vendored
46
debian/changelog
vendored
@ -1,3 +1,49 @@
|
||||
frappy-core (0.17.13) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* add egg-info to gitignore
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* GUI bugfix: use isChecked instead of checkState in BoolInput
|
||||
* frappy_psi.mercury/triton: add control_off command
|
||||
* frappy_psi.phytron: rename reset_error to clear_errors
|
||||
* frappy.mixins.HasOutputModule
|
||||
* frappy_psi.mercury: proper handling of control_active
|
||||
* add a hook for reads to be done initially
|
||||
* frappy_psi.triton: fix HeaterOutput.limit
|
||||
* frappy_psi.magfield: bug fix
|
||||
* frappy_psi.sea: bug fixes
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* server: fix systemd variable scope
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
|
||||
|
||||
frappy-core (0.17.12) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Warn about duplicate module definitions in a file
|
||||
* Add influences property to parameters/commands
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* frappy.client: dummy logger is missing 'exception' method
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* Add SEOP He3-polarization device
|
||||
* Typo in influences description
|
||||
* seop: fix fitparam command
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* silently catches error in systemd.daemon.notify
|
||||
|
||||
[ Alexander Zaft ]
|
||||
* io: add option to retry first ident request
|
||||
* config: fix merge_modules
|
||||
* io: followup fix for retry-first-ident
|
||||
* entangle: fix tango guards for pytango 9.3
|
||||
|
||||
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
|
||||
|
||||
frappy-core (0.17.11) focal; urgency=medium
|
||||
|
||||
[ Alexander Zaft ]
|
||||
|
@ -401,10 +401,15 @@ class UniqueObject:
|
||||
def merge_status(*args):
|
||||
"""merge status
|
||||
|
||||
the status with biggest code wins
|
||||
texts matching maximal code are joined with ', '
|
||||
for combining stati of different mixins
|
||||
- the status with biggest code wins
|
||||
- texts matching maximal code are joined with ', '
|
||||
- if texts already contain ', ', it is considered as composed by
|
||||
individual texts and duplication is avoided. when commas are used
|
||||
for other purposes, the behaviour might be surprising
|
||||
"""
|
||||
maxcode = max(a[0] for a in args)
|
||||
merged = [a[1] for a in args if a[0] == maxcode and a[1]]
|
||||
# use dict instead of set for preserving order
|
||||
merged = {m: True for mm in merged for m in mm.split(', ')}
|
||||
return maxcode, ', '.join(merged)
|
||||
|
@ -640,6 +640,13 @@ class Module(HasAccessibles):
|
||||
all parameters are polled once
|
||||
"""
|
||||
|
||||
def shutdownModule(self):
|
||||
"""called when the sever shuts down
|
||||
|
||||
any cleanup-work should be performed here, like closing threads and
|
||||
saving data.
|
||||
"""
|
||||
|
||||
def doPoll(self):
|
||||
"""polls important parameters like value and status
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
@ -24,6 +23,7 @@
|
||||
"""Define helpers"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
@ -33,7 +33,6 @@ from frappy.dynamic import Pinata
|
||||
from frappy.lib import formatException, generalConfig, get_class, mkthread
|
||||
from frappy.lib.multievent import MultiEvent
|
||||
from frappy.params import PREDEFINED_ACCESSIBLES
|
||||
from frappy.modules import Attached
|
||||
|
||||
try:
|
||||
from daemon import DaemonContext
|
||||
@ -106,6 +105,12 @@ class Server:
|
||||
|
||||
self._cfgfiles = cfgfiles
|
||||
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
def signal_handler(self, _num, _frame):
|
||||
if hasattr(self, 'interface') and self.interface:
|
||||
self.shutdown()
|
||||
|
||||
def start(self):
|
||||
if not DaemonContext:
|
||||
@ -127,17 +132,18 @@ class Server:
|
||||
return f"{cls.__name__} class don't know how to handle option(s): {', '.join(options)}"
|
||||
|
||||
def restart_hook(self):
|
||||
pass
|
||||
"""Actions to be done on restart. May be overridden by a subclass."""
|
||||
|
||||
def run(self):
|
||||
global systemd # pylint: disable=global-statement
|
||||
while self._restart:
|
||||
self._restart = False
|
||||
try:
|
||||
# TODO: make systemd notifications configurable
|
||||
if systemd: # pylint: disable=used-before-assignment
|
||||
if systemd:
|
||||
systemd.daemon.notify("STATUS=initializing")
|
||||
except Exception:
|
||||
systemd = None # pylint: disable=redefined-outer-name
|
||||
systemd = None
|
||||
try:
|
||||
self._processCfg()
|
||||
if self._testonly:
|
||||
@ -156,13 +162,27 @@ class Server:
|
||||
self.log.info('startup done, handling transport messages')
|
||||
if systemd:
|
||||
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
||||
self.interface.serve_forever()
|
||||
self.interface.server_close()
|
||||
t = mkthread(self.interface.serve_forever)
|
||||
# we wait here on the thread finishing, which means we got a
|
||||
# signal to shut down or an exception was raised
|
||||
# TODO: get the exception (and re-raise?)
|
||||
t.join()
|
||||
self.interface = None # fine due to the semantics of 'with'
|
||||
# server_close() called by 'with'
|
||||
|
||||
self.log.info(f'stopped listenning, cleaning up'
|
||||
f' {len(self.modules)} modules')
|
||||
# if systemd:
|
||||
# if self._restart:
|
||||
# systemd.daemon.notify('RELOADING=1')
|
||||
# else:
|
||||
# systemd.daemon.notify('STOPPING=1')
|
||||
for name in self._getSortedModules():
|
||||
self.modules[name].shutdownModule()
|
||||
if self._restart:
|
||||
self.restart_hook()
|
||||
self.log.info('restart')
|
||||
else:
|
||||
self.log.info('shut down')
|
||||
self.log.info('restarting')
|
||||
self.log.info('shut down')
|
||||
|
||||
def restart(self):
|
||||
if not self._restart:
|
||||
@ -268,3 +288,41 @@ class Server:
|
||||
# history_path = os.environ.get('ALTERNATIVE_HISTORY')
|
||||
# if history_path:
|
||||
# from frappy_<xx>.historywriter import ... etc.
|
||||
|
||||
def _getSortedModules(self):
|
||||
"""Sort modules topologically by inverse dependency.
|
||||
|
||||
Example: if there is an IO device A and module B depends on it, then
|
||||
the result will be [B, A].
|
||||
Right now, if the dependency graph is not a DAG, we give up and return
|
||||
the unvisited nodes to be dismantled at the end.
|
||||
Taken from Introduction to Algorithms [CLRS].
|
||||
"""
|
||||
def go(name):
|
||||
if name in done: # visiting a node
|
||||
return True
|
||||
if name in visited:
|
||||
visited.add(name)
|
||||
return False # cycle in dependencies -> fail
|
||||
visited.add(name)
|
||||
if name in unmarked:
|
||||
unmarked.remove(name)
|
||||
for module in self.modules[name].attachedModules.values():
|
||||
res = go(module.name)
|
||||
if not res:
|
||||
return False
|
||||
visited.remove(name)
|
||||
done.add(name)
|
||||
l.append(name)
|
||||
return True
|
||||
|
||||
unmarked = set(self.modules.keys()) # unvisited nodes
|
||||
visited = set() # visited in DFS, but not completed
|
||||
done = set()
|
||||
l = [] # list of sorted modules
|
||||
|
||||
while unmarked:
|
||||
if not go(unmarked.pop()):
|
||||
self.log.error('cyclical dependency between modules!')
|
||||
return l[::-1] + list(visited) + list(unmarked)
|
||||
return l[::-1]
|
||||
|
@ -354,7 +354,7 @@ class Cryostat(CryoBase):
|
||||
timestamp = t
|
||||
self.read_value()
|
||||
|
||||
def shutdown(self):
|
||||
def shutdownModule(self):
|
||||
# should be called from server when the server is stopped
|
||||
self._stopflag = True
|
||||
if self._thread and self._thread.is_alive():
|
||||
|
234
frappy_mlz/seop.py
Normal file
234
frappy_mlz/seop.py
Normal file
@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Georg Brandl <g.brandl@fz-juelich.de>
|
||||
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""Adapter to the existing SEOP 3He spin filter system daemon."""
|
||||
|
||||
from os import path
|
||||
|
||||
# eventually he3control
|
||||
from he3d import he3cell # pylint: disable=import-error
|
||||
|
||||
from frappy.core import Attached
|
||||
from frappy.datatypes import ArrayOf, FloatRange, IntRange, StatusType, \
|
||||
StringType, TupleOf
|
||||
from frappy.errors import CommandRunningError
|
||||
from frappy.modules import Command, Drivable, Module, Parameter, Property, \
|
||||
Readable
|
||||
from frappy.rwhandler import CommonReadHandler
|
||||
|
||||
integral = IntRange()
|
||||
floating = FloatRange()
|
||||
string = StringType()
|
||||
|
||||
# Configuration is kept in YAML files to stay compatible to the
|
||||
# traditional 3He daemon, for now.
|
||||
|
||||
|
||||
class Cell(Module):
|
||||
""" Dummy module for creating He3Cell object in order for other modules to talk to the hardware.
|
||||
Only deals with the config, and rotating the paramlog.
|
||||
"""
|
||||
config_directory = Property(
|
||||
'Directory for the YAML config files', datatype=string)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.cell = he3cell.He3_cell(
|
||||
path.join(self.config_directory, 'cell.yml'))
|
||||
|
||||
# Commands
|
||||
@Command(result=string)
|
||||
def raw_config_file(self):
|
||||
"""return unparsed contents of yaml file"""
|
||||
with open(self.cell._He3_cell__cfg_filename, 'r', encoding='utf-8') as f:
|
||||
return str(f.read())
|
||||
|
||||
@Command(string, result=string)
|
||||
def cfg_get(self, identifier):
|
||||
"""Get a configuration value."""
|
||||
return str(self.cell.cfg_get(identifier))
|
||||
|
||||
@Command((string, string), result=string)
|
||||
def cfg_set(self, identifier, value):
|
||||
"""Set a configuration value."""
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
# The type is lost during transmission.
|
||||
# Check type so the value to be set has the same type and
|
||||
# is not eg. a string where an int would be needed in the config key.
|
||||
oldty = type(self.cell.cfg_get(identifier))
|
||||
if oldty is not type(value):
|
||||
raise ValueError('Type of value to be set does not match the '
|
||||
'value in the configuration!')
|
||||
return str(self.cell.cfg_set(identifier, value))
|
||||
|
||||
@Command()
|
||||
def nmr_paramlog_rotate(self):
|
||||
"""resets fitting and switches to a new logfile"""
|
||||
self.cell.nmr_paramlog_rotate()
|
||||
|
||||
|
||||
class Afp(Readable):
|
||||
"""Polarisation state of the SEOP waveplates"""
|
||||
|
||||
value = Parameter('Current polarisation state of the SEOP waveplates', IntRange(0, 1))
|
||||
|
||||
cell = Attached(Cell)
|
||||
|
||||
def read_value(self):
|
||||
return self.cell.cell.afp_state_get()
|
||||
|
||||
# Commands
|
||||
@Command(description='Flip polarization of SEOP waveplates')
|
||||
def afp_flip(self):
|
||||
self.cell.cell.afp_flip_do()
|
||||
self.read_value()
|
||||
|
||||
|
||||
class Nmr(Readable):
|
||||
Status = Drivable.Status
|
||||
status = Parameter(datatype=StatusType(Drivable.Status))
|
||||
value = Parameter('Timestamp of last NMR', string)
|
||||
cell = Attached(Cell)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.interval = 0
|
||||
|
||||
def read_value(self):
|
||||
return str(self.cell.cell.nmr_timestamp_get())
|
||||
|
||||
def read_status(self):
|
||||
cellstate = self.cell.cell.nmr_state_get()
|
||||
|
||||
if self.cell.cell.nmr_background_check():
|
||||
status = self.Status.BUSY, 'running every %d seconds' % self.interval
|
||||
else:
|
||||
status = self.Status.IDLE, 'not running'
|
||||
|
||||
# TODO: what do we do here with None and -1?
|
||||
# -> None basically indicates that the fit for the parameters did not converge
|
||||
if cellstate is None:
|
||||
return self.Status.IDLE, f'returned None, {status[1]}'
|
||||
if cellstate in (0, 1):
|
||||
return status[0], f'nmr cellstate {cellstate}, {status[1]}'
|
||||
if cellstate == -1:
|
||||
return self.Status.WARN, f'got error from cell, {status[1]}'
|
||||
return self.Status.ERROR, 'Unrecognized cellstate!'
|
||||
|
||||
# Commands
|
||||
@Command()
|
||||
def nmr_do(self):
|
||||
"""Triggers the NMR to run"""
|
||||
self.cell.cell.nmr_do()
|
||||
self.read_status()
|
||||
|
||||
@Command()
|
||||
def bgstart(self):
|
||||
"""Start background NMR"""
|
||||
if self.isBusy():
|
||||
raise CommandRunningError('backgroundNMR is already running')
|
||||
interval = self.cell.cell.cfg_get('tasks/nmr/background/interval')
|
||||
self.interval = interval
|
||||
self.cell.cell.nmr_background_start(interval)
|
||||
self.read_status()
|
||||
|
||||
@Command()
|
||||
def bgstop(self):
|
||||
"""Stop background NMR"""
|
||||
self.cell.cell.nmr_background_stop()
|
||||
self.read_status()
|
||||
|
||||
# Commands to get large datasets we do not want directly in the NICOS cache
|
||||
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||
ArrayOf(floating, maxlen=100000)))
|
||||
def get_processed_nmr(self):
|
||||
"""Get data for processed signal."""
|
||||
val= self.cell.cell.nmr_processed_get()
|
||||
return (val['xval'], val['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||
ArrayOf(floating, maxlen=100000)))
|
||||
def get_raw_nmr(self):
|
||||
"""Get raw signal data."""
|
||||
val = self.cell.cell.nmr_raw_get()
|
||||
return (val['xval'], val['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||
ArrayOf(floating, maxlen=100000)))
|
||||
def get_raw_spectrum(self):
|
||||
"""Get the raw spectrum."""
|
||||
val = self.cell.cell.nmr_raw_spectrum_get()
|
||||
y = val['yval'][:len(val['xval'])]
|
||||
return (val['xval'], y)
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||
ArrayOf(floating, maxlen=100000)))
|
||||
def get_processed_spectrum(self):
|
||||
"""Get the processed spectrum."""
|
||||
val = self.cell.cell.nmr_processed_spectrum_get()
|
||||
x = val['xval'][:len(val['yval'])]
|
||||
return (x, val['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
ArrayOf(floating, maxlen=100)))
|
||||
def get_amplitude(self):
|
||||
"""Last 20 amplitude datapoints."""
|
||||
rv = self.cell.cell.nmr_paramlog_get('amplitude', 20)
|
||||
x = [ str(timestamp) for timestamp in rv['xval']]
|
||||
return (x,rv['yval'])
|
||||
|
||||
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||
ArrayOf(floating, maxlen=100)))
|
||||
def get_phase(self):
|
||||
"""Last 20 phase datapoints."""
|
||||
val = self.cell.cell.nmr_paramlog_get('phase', 20)
|
||||
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
||||
|
||||
|
||||
class FitParam(Readable):
|
||||
value = Parameter('fitted value', unit='$', default=0.0)
|
||||
sigma = Parameter('variance of the fitted value', FloatRange(), default=0.0)
|
||||
param = Property('the parameter that should be accesssed',
|
||||
StringType(), export=False)
|
||||
|
||||
cell = Attached(Cell)
|
||||
|
||||
@CommonReadHandler(['value', 'sigma'])
|
||||
def read_amplitude(self):
|
||||
ret = self.cell.cell.nmr_param_get(self.param)
|
||||
self.value = ret['value']
|
||||
self.sigma = ret['sigma']
|
||||
|
||||
# Commands
|
||||
@Command(integral, result=TupleOf(ArrayOf(string),
|
||||
ArrayOf(floating)))
|
||||
def nmr_paramlog_get(self, n):
|
||||
"""returns the log of the last 'n' values for this parameter"""
|
||||
val = self.cell.cell.nmr_paramlog_get(self.param, n)
|
||||
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
141
test/test_config.py
Normal file
141
test/test_config.py
Normal file
@ -0,0 +1,141 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
# false positive with fixtures
|
||||
# pylint: disable=redefined-outer-name
|
||||
import pytest
|
||||
|
||||
from frappy.config import Collector, Config, Mod, NodeCollector, load_config, \
|
||||
process_file, to_config_path
|
||||
from frappy.errors import ConfigError
|
||||
from frappy.lib import generalConfig
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
pass
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log():
|
||||
return LoggerStub()
|
||||
|
||||
|
||||
PY_FILE = """Node('foonode', 'fodesc', 'fooface')
|
||||
Mod('foo', 'frappy.modules.Readable', 'description', value=5)
|
||||
Mod('bar', 'frappy.modules.Readable', 'about me', export=False)
|
||||
Mod('baz', 'frappy.modules.Readable', 'things', value=Param(3, unit='BAR'))
|
||||
"""
|
||||
|
||||
|
||||
# fixture file system, TODO: make a bit nicer?
|
||||
@pytest.fixture
|
||||
def direc(tmp_path_factory):
|
||||
d = tmp_path_factory.mktemp('cfgdir')
|
||||
a = d / 'a'
|
||||
b = d / 'b'
|
||||
a.mkdir()
|
||||
b.mkdir()
|
||||
f = a / 'config_cfg.py'
|
||||
pyfile = a / 'pyfile_cfg.py'
|
||||
ff = b / 'test_cfg.py'
|
||||
fff = b / 'alsoworks.py'
|
||||
f.touch()
|
||||
ff.touch()
|
||||
fff.touch()
|
||||
pyfile.write_text(PY_FILE)
|
||||
generalConfig.testinit(confdir=f'{a}:{b}', piddir=str(d))
|
||||
return d
|
||||
|
||||
|
||||
files = [('config', 'a/config_cfg.py'),
|
||||
('config_cfg', 'a/config_cfg.py'),
|
||||
('config_cfg.py', 'a/config_cfg.py'),
|
||||
('test', 'b/test_cfg.py'),
|
||||
('test_cfg', 'b/test_cfg.py'),
|
||||
('test_cfg.py', 'b/test_cfg.py'),
|
||||
('alsoworks', 'b/alsoworks.py'),
|
||||
('alsoworks.py', 'b/alsoworks.py'),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file, res', files)
|
||||
def test_to_cfg_path(log, direc, file, res):
|
||||
assert to_config_path(file, log).endswith(res)
|
||||
|
||||
|
||||
def test_cfg_not_existing(direc, log):
|
||||
with pytest.raises(ConfigError):
|
||||
to_config_path('idonotexist', log)
|
||||
|
||||
|
||||
def collector_helper(node, mods):
|
||||
n = NodeCollector()
|
||||
n.add(*node)
|
||||
m = Collector(Mod)
|
||||
m.list = [Mod(module, '', '') for module in mods]
|
||||
return n, m
|
||||
|
||||
|
||||
configs = [
|
||||
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||
['foo', 'more', 'other'], ['n1', 'iface', 5, {'foo'}]),
|
||||
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||
['different', 'more', 'other'], ['n1', 'iface', 6, set()]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('n1, m1, n2, m2, res', configs)
|
||||
def test_merge(n1, m1, n2, m2, res):
|
||||
name, iface, num_mods, ambig = res
|
||||
c1 = Config(*collector_helper(n1, m1))
|
||||
c2 = Config(*collector_helper(n2, m2))
|
||||
c1.merge_modules(c2)
|
||||
assert c1['node']['equipment_id'] == name
|
||||
assert c1['node']['interface'] == iface
|
||||
assert len(c1.module_names) == num_mods
|
||||
assert c1.ambiguous == ambig
|
||||
|
||||
|
||||
def do_asserts(ret):
|
||||
assert len(ret.module_names) == 3
|
||||
assert set(ret.module_names) == set(['foo', 'bar', 'baz'])
|
||||
assert ret['node']['equipment_id'] == 'foonode'
|
||||
assert ret['node']['interface'] == 'fooface'
|
||||
assert ret['foo'] == {'cls': 'frappy.modules.Readable',
|
||||
'description': 'description', 'value': {'value': 5}}
|
||||
assert ret['bar'] == {'cls': 'frappy.modules.Readable',
|
||||
'description': 'about me', 'export': {'value': False}}
|
||||
assert ret['baz'] == {'cls': 'frappy.modules.Readable',
|
||||
'description': 'things',
|
||||
'value': {'value': 3, 'unit': 'BAR'}}
|
||||
|
||||
|
||||
def test_process_file(direc, log):
|
||||
ret = process_file(str(direc / 'a' / 'pyfile_cfg.py'), log)
|
||||
do_asserts(ret)
|
||||
|
||||
|
||||
def test_full(direc, log):
|
||||
ret = load_config('pyfile_cfg.py', log)
|
||||
do_asserts(ret)
|
@ -22,7 +22,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from frappy.lib import parse_host_port
|
||||
from frappy.lib import parse_host_port, merge_status
|
||||
|
||||
|
||||
@pytest.mark.parametrize('hostport, defaultport, result', [
|
||||
@ -46,3 +46,19 @@ def test_parse_host(hostport, defaultport, result):
|
||||
parse_host_port(hostport, defaultport)
|
||||
else:
|
||||
assert result == parse_host_port(hostport, defaultport)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('args, result', [
|
||||
([(100, 'idle'), (200, 'warning')],
|
||||
(200, 'warning')),
|
||||
([(300, 'ramping'), (300, 'within tolerance')],
|
||||
(300, 'ramping, within tolerance')),
|
||||
([(300, 'ramping, within tolerance'), (300, 'within tolerance, slow'), (200, 'warning')],
|
||||
(300, 'ramping, within tolerance, slow')),
|
||||
# when a comma is used for other purposes than separating individual status texts,
|
||||
# the behaviour might not be as desired. However, this case is somewhat constructed.
|
||||
([(100, 'blue, yellow is my favorite'), (100, 'white, blue, red is a bad color mix')],
|
||||
(100, 'blue, yellow is my favorite, white, red is a bad color mix')),
|
||||
])
|
||||
def test_merge_status(args, result):
|
||||
assert merge_status(*args) == result
|
||||
|
55
test/test_server.py
Normal file
55
test/test_server.py
Normal file
@ -0,0 +1,55 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import pytest
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from frappy.server import Server
|
||||
|
||||
from .test_config import direc # pylint: disable=unused-import
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
pass
|
||||
|
||||
def getChild(self, *args):
|
||||
return self
|
||||
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log():
|
||||
return LoggerStub()
|
||||
|
||||
|
||||
def test_name_only(direc, log):
|
||||
"""only see that this does not throw. get config from name."""
|
||||
s = Server('pyfile', log)
|
||||
s._processCfg()
|
||||
|
||||
|
||||
def test_file(direc, log):
|
||||
"""only see that this does not throw. get config from cfgfiles."""
|
||||
s = Server('foo', log, cfgfiles='pyfile_cfg.py')
|
||||
s._processCfg()
|
Loading…
x
Reference in New Issue
Block a user