diff --git a/bin/frappy-cfg-editor b/bin/frappy-cfg-editor index fbc8ae7..a2c58fa 100755 --- a/bin/frappy-cfg-editor +++ b/bin/frappy-cfg-editor @@ -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 diff --git a/bin/frappy-server b/bin/frappy-server index 3d6993f..d1c5cf9 100755 --- a/bin/frappy-server +++ b/bin/frappy-server @@ -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 .\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__': diff --git a/cfg/seop_cfg.py b/cfg/seop_cfg.py new file mode 100644 index 0000000..8a8c024 --- /dev/null +++ b/cfg/seop_cfg.py @@ -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, + ) diff --git a/debian/changelog b/debian/changelog index 8166be0..307cfad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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 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 Tue, 13 Jun 2023 06:51:27 +0200 + frappy-core (0.17.11) focal; urgency=medium [ Alexander Zaft ] diff --git a/frappy/lib/__init__.py b/frappy/lib/__init__.py index 6e639a9..a63eebe 100644 --- a/frappy/lib/__init__.py +++ b/frappy/lib/__init__.py @@ -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) diff --git a/frappy/modules.py b/frappy/modules.py index bf3a661..fc57d1d 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -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 diff --git a/frappy/server.py b/frappy/server.py index 0cec1f6..5152728 100644 --- a/frappy/server.py +++ b/frappy/server.py @@ -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_.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] diff --git a/frappy_demo/cryo.py b/frappy_demo/cryo.py index c996422..b357cc4 100644 --- a/frappy_demo/cryo.py +++ b/frappy_demo/cryo.py @@ -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(): diff --git a/frappy_mlz/seop.py b/frappy_mlz/seop.py new file mode 100644 index 0000000..c193806 --- /dev/null +++ b/frappy_mlz/seop.py @@ -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 +# Alexander Zaft +# +# ***************************************************************************** + +"""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']) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..5095a76 --- /dev/null +++ b/test/test_config.py @@ -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 +# +# ***************************************************************************** + +# 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) diff --git a/test/test_lib.py b/test/test_lib.py index 03c09a9..0fb85e0 100644 --- a/test/test_lib.py +++ b/test/test_lib.py @@ -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 diff --git a/test/test_server.py b/test/test_server.py new file mode 100644 index 0000000..fad6225 --- /dev/null +++ b/test/test_server.py @@ -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 +# +# ***************************************************************************** + +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()