more merges from gerrit
Change-Id: I13441cd8889dd39f74a2dd1a85e75a1b76bb93c8
This commit is contained in:
parent
10018b8cad
commit
34b93adef0
@ -30,8 +30,8 @@ from os import path
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
||||
|
||||
from secop.lib import getGeneralConfig
|
||||
from secop.logging import initLogging
|
||||
from secop.lib import generalConfig
|
||||
from secop.logging import logger
|
||||
from secop.server import Server
|
||||
|
||||
|
||||
@ -75,6 +75,11 @@ def parseArgv(argv):
|
||||
action='store_true',
|
||||
help='check cfg files only',
|
||||
default=False)
|
||||
parser.add_argument('-r',
|
||||
'--relaxed',
|
||||
action='store_true',
|
||||
help='no checking of problematic behaviour',
|
||||
default=False)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
@ -85,9 +90,15 @@ def main(argv=None):
|
||||
args = parseArgv(argv[1:])
|
||||
|
||||
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
||||
getGeneralConfig(args.gencfg)
|
||||
log = initLogging(loglevel)
|
||||
srv = Server(args.name, log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||
if args.relaxed:
|
||||
generalConfig.defaults['lazy_number_validation'] = True
|
||||
generalConfig.defaults['disable_value_range_check'] = True
|
||||
generalConfig.defaults['legacy_hasiodev'] = True
|
||||
generalConfig.defaults['tolerate_poll_property'] = True
|
||||
generalConfig.init(args.gencfg)
|
||||
logger.init(loglevel)
|
||||
|
||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||
|
||||
if args.daemonize:
|
||||
srv.start()
|
||||
|
40
ci/Jenkinsfile
vendored
40
ci/Jenkinsfile
vendored
@ -30,6 +30,8 @@ def changedFiles = '';
|
||||
|
||||
def run_pylint(pyver) {
|
||||
stage ('pylint-' + pyver) {
|
||||
def cpylint = "RUNNING"
|
||||
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||
def status = 'OK'
|
||||
changedFiles = sh returnStdout: true, script: '''\
|
||||
#!/bin/bash
|
||||
@ -56,8 +58,8 @@ fi
|
||||
withCredentials([string(credentialsId: 'GERRITHTTP',
|
||||
variable: 'GERRITHTTP')]) {
|
||||
sh """\
|
||||
#!/bin/bash
|
||||
if [ -f pylint_results.txt ] ; then
|
||||
#!/bin/bash
|
||||
if [ -f pylint_results.txt ] ; then
|
||||
/home/jenkins/tools2/bin/pylint2gerrit
|
||||
mv pylint_results.txt pylint-${pyver}.txt
|
||||
else
|
||||
@ -68,18 +70,15 @@ fi
|
||||
|
||||
echo "pylint result: $res"
|
||||
this.verifyresult.put('pylint'+pyver, 1)
|
||||
cpylint = "SUCCESSFUL"
|
||||
if ( res != 0 ) {
|
||||
currentBuild.result='FAILURE'
|
||||
this.verifyresult.put('pylint'+ pyver, -1)
|
||||
status = 'FAILURE'
|
||||
cpylint = "FAILED"
|
||||
}
|
||||
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: this.verifyresult['pylint'+pyver],
|
||||
verifyStatusCategory: 'pylint ',
|
||||
verifyStatusName: 'pylint-'+pyver,
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '!recheck'])
|
||||
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||
archiveArtifacts([allowEmptyArchive: true,
|
||||
artifacts: 'pylint-*.txt'])
|
||||
recordIssues([enabledForFailure: true,
|
||||
@ -99,7 +98,9 @@ fi
|
||||
|
||||
def run_tests(pyver) {
|
||||
stage('Test:' + pyver) {
|
||||
writeFile file: 'setup.cfg', text: '''
|
||||
def cpytest = "RUNNING"
|
||||
gerritPostCheck(["jenkins:pytest_${pyver}":"RUNNING"])
|
||||
writeFile file: 'setup.cfg', text: '''
|
||||
[tool:pytest]
|
||||
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
|
||||
|
||||
@ -116,18 +117,15 @@ python3 setup.py develop
|
||||
make test
|
||||
'''
|
||||
verifyresult.put(pyver, 1)
|
||||
cpytest = "SUCCESSFUL"
|
||||
}
|
||||
} catch (all) {
|
||||
currentBuild.result = 'FAILURE'
|
||||
status = 'FAILURE'
|
||||
cpytest= "FAILED"
|
||||
verifyresult.put(pyver, -1)
|
||||
}
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: verifyresult[pyver],
|
||||
verifyStatusCategory: 'test ',
|
||||
verifyStatusName: 'pytest-'+pyver,
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '!recheck'])
|
||||
gerritPostCheck(["jenkins:pytest_${pyver}":cpytest])
|
||||
|
||||
step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
|
||||
keepLongStdio: true, testResults: 'pytest.xml'])
|
||||
@ -138,6 +136,8 @@ make test
|
||||
}
|
||||
|
||||
def run_docs() {
|
||||
def cdocs = "RUNNING"
|
||||
gerritPostCheck(["jenkins:docs":cdocs])
|
||||
stage('prepare') {
|
||||
sh '''
|
||||
. /home/jenkins/secopvenv/bin/activate
|
||||
@ -185,15 +185,9 @@ def run_docs() {
|
||||
|
||||
stage('store html doc for build') {
|
||||
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: 1,
|
||||
verifyStatusCategory: 'test ',
|
||||
verifyStatusName: 'doc',
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '@recheck'
|
||||
])
|
||||
cdocs = "SUCCESSFUL"
|
||||
}
|
||||
|
||||
gerritPostCheck(["jenkins:docs":cdocs])
|
||||
}
|
||||
|
||||
|
||||
|
170
debian/changelog
vendored
170
debian/changelog
vendored
@ -1,3 +1,111 @@
|
||||
secop-core (0.12.4) focal; urgency=medium
|
||||
|
||||
* fix command inheritance
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Thu, 11 Nov 2021 16:21:19 +0100
|
||||
|
||||
secop-core (0.12.3) focal; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* Makefile: fix docker image
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* various fixes
|
||||
* remove irrelevant comments
|
||||
* introduce BytesIO
|
||||
* GUI fixes
|
||||
* persistent params / trinamic motor
|
||||
* fix Parameter/Command copy method
|
||||
* show first instead of last traceback on multiple errors
|
||||
* fix parameter inheritance
|
||||
* fix property inheritance
|
||||
* fix python 3.5 compatibility
|
||||
* omit updates of unchanged values within short time
|
||||
* improve simulation
|
||||
* automatically register subclasses of AsynConn
|
||||
* fix feature for removing commands
|
||||
|
||||
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
|
||||
|
||||
secop-core (0.12.2) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* fix issue with new syntax in simulation
|
||||
* treat specifier of describe message
|
||||
* allow to remove accessibles
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* secop_mlz: small fixes
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
|
||||
|
||||
secop-core (0.12.1) focal; urgency=medium
|
||||
|
||||
* remove secop-console from debian *.install file
|
||||
|
||||
-- Enrico Faulhaber <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 09:42:53 +0200
|
||||
|
||||
secop-core (0.12.0) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* make datatypes immutable
|
||||
* customizable general config
|
||||
* support for multiple secop servers
|
||||
* secop.asynconn without pyserial
|
||||
* change cfg file format
|
||||
* fix bug in secop.gui.valuewidgets
|
||||
* fix deadlock when reconnecting client
|
||||
* allow class instead of class name in proxy_class
|
||||
* fix pylint command in Makefile
|
||||
* change arguments of stringio-server
|
||||
* router bug fix
|
||||
* introduce update callbacks
|
||||
* improve error handling on client connections
|
||||
* ppms: improve status and temperature
|
||||
* rework tcp server
|
||||
* cosmetics on datatypes.TextType
|
||||
* improve error handling in SecopClient
|
||||
* improve HasIodev
|
||||
* HasIodev bug fix
|
||||
* fix handling of StructOf datatype
|
||||
* more flexible end_of_line in stringio
|
||||
* improvements on PPMS and LS370
|
||||
* add readbytes method to AsynConn
|
||||
* Param(..., initwrite=True) works only with poll=True
|
||||
* fix initwrite behaviour
|
||||
* make order of accessibles work again
|
||||
* main module of LS370 is now drivable
|
||||
* improve softcal
|
||||
* make arguments of Parameter and Override consistent
|
||||
* new syntax for parameter/commands/properties
|
||||
* enhance documentation
|
||||
* removed old style syntax
|
||||
* after running isort
|
||||
* try to follow PEP8
|
||||
* fix inheritance order
|
||||
* remove obsolete code
|
||||
* lookup cfg files in a list of directories
|
||||
* added hook for optional history writer
|
||||
* fixed errors during migration
|
||||
* move historywriter to secop_psi
|
||||
* fix autoscan behaviour in ls370res
|
||||
|
||||
[ l_samenv ]
|
||||
* improve tutorial_helevel
|
||||
* fixed bugs from syntax migration
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* user friendly reporting of config errors
|
||||
|
||||
[ Bjoern Pedersen ]
|
||||
* Jenkisfile: verification
|
||||
* Fixes to Jenkinsfile
|
||||
* No pull for images, they are recreated in the job
|
||||
* Another Jenkisfile error
|
||||
* Correct checks enum
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 08:49:57 +0200
|
||||
|
||||
secop-core (0.11.6) unstable; urgency=medium
|
||||
|
||||
* fix secop-generator
|
||||
@ -133,7 +241,7 @@ secop-core (0.10.5) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 29 Oct 2019 16:33:18 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 29 Oct 2019 16:33:18 +0100
|
||||
|
||||
secop-core (0.10.3) unstable; urgency=low
|
||||
|
||||
@ -142,7 +250,7 @@ secop-core (0.10.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:49:43 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:49:43 +0200
|
||||
|
||||
secop-core (0.10.2) unstable; urgency=low
|
||||
|
||||
@ -153,7 +261,7 @@ secop-core (0.10.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:42:58 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:42:58 +0200
|
||||
|
||||
secop-core (0.10.1) unstable; urgency=low
|
||||
|
||||
@ -162,7 +270,7 @@ secop-core (0.10.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:41:10 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:41:10 +0200
|
||||
|
||||
secop-core (0.10.0) unstable; urgency=low
|
||||
|
||||
@ -171,7 +279,7 @@ secop-core (0.10.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:31:14 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:31:14 +0200
|
||||
|
||||
secop-core (0.9.0) unstable; urgency=low
|
||||
|
||||
@ -198,7 +306,7 @@ secop-core (0.9.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:26:07 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:26:07 +0200
|
||||
|
||||
secop-core (0.8.1) unstable; urgency=low
|
||||
|
||||
@ -207,7 +315,7 @@ secop-core (0.8.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 15:40:44 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 15:40:44 +0200
|
||||
|
||||
secop-core (0.8.0) unstable; urgency=low
|
||||
|
||||
@ -275,7 +383,7 @@ secop-core (0.8.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 10:27:51 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 10:27:51 +0200
|
||||
|
||||
secop-core (0.7.0) unstable; urgency=low
|
||||
|
||||
@ -311,7 +419,7 @@ secop-core (0.7.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 28 Mar 2019 13:46:08 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 28 Mar 2019 13:46:08 +0100
|
||||
|
||||
secop-core (0.6.4) unstable; urgency=low
|
||||
|
||||
@ -376,7 +484,7 @@ secop-core (0.6.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 20 Dec 2018 16:44:03 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 20 Dec 2018 16:44:03 +0100
|
||||
|
||||
secop-core (0.6.3) unstable; urgency=low
|
||||
|
||||
@ -390,7 +498,7 @@ secop-core (0.6.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 27 Jul 2018 09:31:59 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 27 Jul 2018 09:31:59 +0200
|
||||
|
||||
secop-core (0.6.2) unstable; urgency=low
|
||||
|
||||
@ -429,7 +537,7 @@ secop-core (0.6.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 18 Jul 2018 12:06:57 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 18 Jul 2018 12:06:57 +0200
|
||||
|
||||
secop-core (0.6.1) unstable; urgency=low
|
||||
|
||||
@ -438,7 +546,7 @@ secop-core (0.6.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 19 Apr 2018 10:24:44 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 19 Apr 2018 10:24:44 +0200
|
||||
|
||||
secop-core (0.6.0) unstable; urgency=low
|
||||
|
||||
@ -458,7 +566,7 @@ secop-core (0.6.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 17:38:52 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 17:38:52 +0200
|
||||
|
||||
secop-core (0.5.0) unstable; urgency=low
|
||||
|
||||
@ -521,7 +629,7 @@ secop-core (0.5.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 12:45:58 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 12:45:58 +0200
|
||||
|
||||
secop-core (0.4.4) unstable; urgency=low
|
||||
|
||||
@ -530,7 +638,7 @@ secop-core (0.4.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Sun, 24 Sep 2017 22:25:01 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Sun, 24 Sep 2017 22:25:01 +0200
|
||||
|
||||
secop-core (0.4.3) unstable; urgency=low
|
||||
|
||||
@ -539,7 +647,7 @@ secop-core (0.4.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 17:29:46 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 17:29:46 +0200
|
||||
|
||||
secop-core (0.4.2) unstable; urgency=low
|
||||
|
||||
@ -548,7 +656,7 @@ secop-core (0.4.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 16:37:59 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 16:37:59 +0200
|
||||
|
||||
secop-core (0.4.1) unstable; urgency=low
|
||||
|
||||
@ -557,7 +665,7 @@ secop-core (0.4.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 13:25:28 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 13:25:28 +0200
|
||||
|
||||
secop-core (0.4.0) unstable; urgency=low
|
||||
|
||||
@ -567,7 +675,7 @@ secop-core (0.4.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 10:33:04 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 10:33:04 +0200
|
||||
|
||||
secop-core (0.3.0) unstable; urgency=low
|
||||
|
||||
@ -633,7 +741,7 @@ secop-core (0.3.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Mon, 18 Sep 2017 14:18:36 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Mon, 18 Sep 2017 14:18:36 +0200
|
||||
|
||||
secop-core (0.2.0) unstable; urgency=low
|
||||
|
||||
@ -642,7 +750,7 @@ secop-core (0.2.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 14:55:41 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 14:55:41 +0200
|
||||
|
||||
secop-core (0.1.1) unstable; urgency=low
|
||||
|
||||
@ -651,7 +759,7 @@ secop-core (0.1.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 11:02:19 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 11:02:19 +0200
|
||||
|
||||
secop-core (0.1.0) unstable; urgency=low
|
||||
|
||||
@ -660,7 +768,7 @@ secop-core (0.1.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 10:50:24 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 10:50:24 +0200
|
||||
|
||||
secop-core (0.0.8) unstable; urgency=low
|
||||
|
||||
@ -669,7 +777,7 @@ secop-core (0.0.8) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 14:13:11 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 14:13:11 +0200
|
||||
|
||||
secop-core (0.0.7) unstable; urgency=low
|
||||
|
||||
@ -678,7 +786,7 @@ secop-core (0.0.7) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:52:15 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:52:15 +0200
|
||||
|
||||
secop-core (0.0.6) unstable; urgency=low
|
||||
|
||||
@ -688,7 +796,7 @@ secop-core (0.0.6) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:39:07 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:39:07 +0200
|
||||
|
||||
secop-core (0.0.5) unstable; urgency=low
|
||||
|
||||
@ -697,7 +805,7 @@ secop-core (0.0.5) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:11:43 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:11:43 +0200
|
||||
|
||||
secop-core (0.0.4) unstable; urgency=low
|
||||
|
||||
@ -706,7 +814,7 @@ secop-core (0.0.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:39:42 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:39:42 +0200
|
||||
|
||||
secop-core (0.0.3) unstable; urgency=low
|
||||
|
||||
@ -716,7 +824,7 @@ secop-core (0.0.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:27:28 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:27:28 +0200
|
||||
|
||||
secop-core (0.0.2) unstable; urgency=medium
|
||||
|
||||
@ -794,4 +902,4 @@ secop-core (0.0.2) unstable; urgency=medium
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 19 Jul 2017 11:44:13 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 19 Jul 2017 11:44:13 +0200
|
||||
|
1
debian/secop-core.install
vendored
1
debian/secop-core.install
vendored
@ -1,5 +1,4 @@
|
||||
usr/bin/secop-server
|
||||
usr/bin/secop-console
|
||||
usr/lib/python3.*/dist-packages/secop/*.py
|
||||
usr/lib/python3.*/dist-packages/secop/lib
|
||||
usr/lib/python3.*/dist-packages/secop/client
|
||||
|
@ -4,6 +4,8 @@ Reference
|
||||
Module Base Classes
|
||||
...................
|
||||
|
||||
.. autodata:: secop.modules.Done
|
||||
|
||||
.. autoclass:: secop.modules.Module
|
||||
:members: earlyInit, initModule, startModule, pollerClass
|
||||
|
||||
@ -49,11 +51,15 @@ Communication
|
||||
:show-inheritance:
|
||||
:members: communicate
|
||||
|
||||
.. autoclass:: secop.stringio.StringIO
|
||||
.. autoclass:: secop.io.StringIO
|
||||
:show-inheritance:
|
||||
:members: communicate, multicomm
|
||||
|
||||
.. autoclass:: secop.stringio.HasIodev
|
||||
.. autoclass:: secop.io.BytesIO
|
||||
:show-inheritance:
|
||||
:members: communicate, multicomm
|
||||
|
||||
.. autoclass:: secop.io.HasIO
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: secop.iohandler.IOHandlerBase
|
||||
|
@ -22,7 +22,7 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
.. code:: python
|
||||
|
||||
# the most common Frappy classes can be imported from secop.core
|
||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
@ -34,14 +34,13 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||
# for talking with the hardware
|
||||
# inheriting HasIO allows us to use the communicate method for talking with the hardware
|
||||
# Readable as a base class defines the value and status parameters
|
||||
class HeLevel(HasIodev, Readable):
|
||||
class HeLevel(HasIO, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
# define the communication class to create the IO module
|
||||
iodevClass = CCU4IO
|
||||
ioClass = CCU4IO
|
||||
|
||||
# define or alter the parameters
|
||||
# as Readable.value exists already, we give only the modified property 'unit'
|
||||
@ -49,7 +48,7 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
|
||||
def read_value(self):
|
||||
# method for reading the main value
|
||||
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||
reply = self.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||
name, txtvalue = reply.split('=')
|
||||
assert name == 'h' # check that we got a reply to our command
|
||||
return txtvalue # the framework will automatically convert the string to a float
|
||||
@ -115,17 +114,17 @@ the status codes from the hardware to the standard SECoP status codes.
|
||||
}
|
||||
|
||||
def read_status(self):
|
||||
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
||||
name, txtvalue = self.communicate('hsf').split('=')
|
||||
assert name == 'hsf'
|
||||
return self.STATUS_MAP(int(txtvalue))
|
||||
|
||||
def read_empty_length(self):
|
||||
name, txtvalue = self._iodev.communicate('hem').split('=')
|
||||
name, txtvalue = self.communicate('hem').split('=')
|
||||
assert name == 'hem'
|
||||
return txtvalue
|
||||
|
||||
def write_empty_length(self, value):
|
||||
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
|
||||
name, txtvalue = self.communicate('hem=%g' % value).split('=')
|
||||
assert name == 'hem'
|
||||
return txtvalue
|
||||
|
||||
@ -152,7 +151,7 @@ which means it might be worth to create a *query* method, and then the
|
||||
for changing a parameter
|
||||
:returns: the (new) value of the parameter
|
||||
"""
|
||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||
name, txtvalue = self.communicate(cmd).split('=')
|
||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||
|
||||
|
@ -111,6 +111,7 @@ class Cryostat(CryoBase):
|
||||
group='stability')
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._stopflag = False
|
||||
self._thread = mkthread(self.thread)
|
||||
|
||||
|
@ -133,6 +133,7 @@ class MagneticField(Drivable):
|
||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
@ -235,6 +236,7 @@ class SampleTemp(Drivable):
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
@ -31,7 +31,7 @@ import math
|
||||
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.errors import ConfigError, DisabledError
|
||||
from secop.lib.sequence import SequencerMixin, Step
|
||||
from secop.modules import BasicPoller, Drivable, Parameter
|
||||
from secop.modules import Drivable, Parameter
|
||||
|
||||
|
||||
class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
the symmetry setting selects which.
|
||||
"""
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
|
||||
# parameters
|
||||
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||
@ -57,10 +54,10 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(-0.5, 0.5), poll=True,
|
||||
default=(-0.5, 0.5),
|
||||
)
|
||||
precision = Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
calibration = Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
' in T',
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
@ -137,7 +134,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
'_current2field polynome not monotonic!')
|
||||
|
||||
def initModule(self):
|
||||
super(GarfieldMagnet, self).initModule()
|
||||
super().initModule()
|
||||
self._enable = self.DISPATCHER.get_module(self.subdev_enable)
|
||||
self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry)
|
||||
self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch)
|
||||
@ -220,7 +217,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
self._currentsource.read_value() *
|
||||
self._get_field_polarity())
|
||||
|
||||
def read_hw_status(self):
|
||||
def readHwStatus(self):
|
||||
# called from SequencerMixin.read_status if no sequence is running
|
||||
if self._enable.value == 'Off':
|
||||
return self.Status.WARN, 'Disabled'
|
||||
|
@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \
|
||||
from secop.errors import CommunicationFailedError, \
|
||||
ConfigError, HardwareError, ProgrammingError
|
||||
from secop.lib import lazy_property
|
||||
from secop.modules import BasicPoller, Command, \
|
||||
from secop.modules import Command, \
|
||||
Drivable, Module, Parameter, Readable
|
||||
|
||||
#####
|
||||
@ -157,8 +157,6 @@ class PyTangoDevice(Module):
|
||||
execution and attribute operations with logging and exception mapping.
|
||||
"""
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
# parameters
|
||||
comtries = Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
@ -210,7 +208,7 @@ class PyTangoDevice(Module):
|
||||
# exception mapping is enabled).
|
||||
self._createPyTangoDevice = self._applyGuardToFunc(
|
||||
self._createPyTangoDevice, 'constructor')
|
||||
super(PyTangoDevice, self).earlyInit()
|
||||
super().earlyInit()
|
||||
|
||||
@lazy_property
|
||||
def _dev(self):
|
||||
@ -249,10 +247,10 @@ class PyTangoDevice(Module):
|
||||
# otherwise would lead to attribute errors later
|
||||
try:
|
||||
device.State
|
||||
except AttributeError:
|
||||
except AttributeError as e:
|
||||
raise CommunicationFailedError(
|
||||
self, 'connection to Tango server failed, '
|
||||
'is the server running?')
|
||||
'is the server running?') from e
|
||||
return self._applyGuardsToPyTangoDevice(device)
|
||||
|
||||
def _applyGuardsToPyTangoDevice(self, dev):
|
||||
@ -376,14 +374,17 @@ class AnalogInput(PyTangoDevice, Readable):
|
||||
The AnalogInput handles all devices only delivering an analogue value.
|
||||
"""
|
||||
|
||||
def startModule(self, started_callback):
|
||||
super(AnalogInput, self).startModule(started_callback)
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
# update
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
try:
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
# update
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
def read_value(self):
|
||||
return self._dev.value
|
||||
@ -422,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')),
|
||||
readonly=False, poll=10,
|
||||
readonly=False,
|
||||
)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
@ -446,13 +447,13 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
_moving = False
|
||||
|
||||
def initModule(self):
|
||||
super(AnalogOutput, self).initModule()
|
||||
super().initModule()
|
||||
# init history
|
||||
self._history = [] # will keep (timestamp, value) tuple
|
||||
self._timeout = None # keeps the time at which we will timeout, or None
|
||||
|
||||
def startModule(self, started_callback):
|
||||
super(AnalogOutput, self).startModule(started_callback)
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
@ -460,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
|
||||
def pollParams(self, nr=0):
|
||||
super(AnalogOutput, self).pollParams(nr)
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
while len(self._history) > 2:
|
||||
# if history would be too short, break
|
||||
if self._history[-1][0] - self._history[1][0] <= self.window:
|
||||
@ -489,8 +490,11 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
hist = self._history[:]
|
||||
window_start = currenttime() - self.window
|
||||
hist_in_window = [v for (t, v) in hist if t >= window_start]
|
||||
if len(hist) == len(hist_in_window):
|
||||
return False # no data point before window
|
||||
if not hist_in_window:
|
||||
return False # no relevant history -> no knowledge
|
||||
# window is too small -> use last point only
|
||||
hist_in_window = [self.value]
|
||||
|
||||
max_in_hist = max(hist_in_window)
|
||||
min_in_hist = min(hist_in_window)
|
||||
@ -503,13 +507,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if self._isAtTarget():
|
||||
self._timeout = None
|
||||
self._moving = False
|
||||
return super(AnalogOutput, self).read_status()
|
||||
if self._timeout:
|
||||
if self._timeout < currenttime():
|
||||
return self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
||||
if self._moving:
|
||||
return (self.Status.BUSY, 'moving')
|
||||
return (self.Status.IDLE, 'stable')
|
||||
status = super().read_status()
|
||||
else:
|
||||
if self._timeout and self._timeout < currenttime():
|
||||
status = self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
||||
else:
|
||||
status = (self.Status.BUSY, 'moving') if self._moving else (self.Status.IDLE, 'stable')
|
||||
self.setFastPoll(self.isBusy(status))
|
||||
return status
|
||||
|
||||
@property
|
||||
def absmin(self):
|
||||
@ -571,11 +576,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if not self.timeout:
|
||||
self._timeout = None
|
||||
self._moving = True
|
||||
self._history = [] # clear history
|
||||
self.read_status() # poll our status to keep it updated
|
||||
# do not clear the history here:
|
||||
# - if the target is not changed by more than precision, there is no need to wait
|
||||
# self._history = []
|
||||
self.read_status() # poll our status to keep it updated (this will also set fast poll)
|
||||
return self.read_target()
|
||||
|
||||
def _hw_wait(self):
|
||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||
while super().read_status()[0] == self.Status.BUSY:
|
||||
sleep(0.3)
|
||||
|
||||
def stop(self):
|
||||
@ -597,8 +605,7 @@ class Actuator(AnalogOutput):
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
)
|
||||
ramp = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
readonly=False, datatype=FloatRange(0, unit='$/min'),
|
||||
)
|
||||
|
||||
def read_speed(self):
|
||||
@ -677,17 +684,22 @@ class TemperatureController(Actuator):
|
||||
)
|
||||
pid = Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
readonly=False, group='pid',
|
||||
)
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'),
|
||||
)
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(),
|
||||
)
|
||||
|
||||
# overrides
|
||||
precision = Parameter(default=0.1)
|
||||
ramp = Parameter(description='Temperature ramp')
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self.read_setpoint()
|
||||
self.read_heateroutput()
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
|
||||
@ -730,6 +742,10 @@ class TemperatureController(Actuator):
|
||||
def read_heateroutput(self):
|
||||
return self._dev.heaterOutput
|
||||
|
||||
# remove UserCommand setposition from Actuator
|
||||
# (makes no sense for a TemperatureController)
|
||||
setposition = None
|
||||
|
||||
|
||||
class PowerSupply(Actuator):
|
||||
"""A power supply (voltage and current) device.
|
||||
@ -737,13 +753,19 @@ class PowerSupply(Actuator):
|
||||
|
||||
# parameters
|
||||
voltage = Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5)
|
||||
datatype=FloatRange(unit='V'))
|
||||
current = Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5)
|
||||
datatype=FloatRange(unit='A'))
|
||||
|
||||
# overrides
|
||||
ramp = Parameter(description='Current/voltage ramp')
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
# TODO: poll voltage and current faster when busy
|
||||
self.read_voltage()
|
||||
self.read_current()
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
|
||||
@ -777,16 +799,18 @@ class NamedDigitalInput(DigitalInput):
|
||||
datatype=StringType(), export=False) # XXX:!!!
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalInput, self).initModule()
|
||||
super().initModule()
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
mapping = self.mapping
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(mapping)
|
||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||
except Exception as e:
|
||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
||||
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||
|
||||
def read_value(self):
|
||||
value = self._dev.value
|
||||
@ -805,7 +829,7 @@ class PartialDigitalInput(NamedDigitalInput):
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalInput, self).initModule()
|
||||
super().initModule()
|
||||
self._mask = (1 << self.bitwidth) - 1
|
||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||
|
||||
@ -827,9 +851,16 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
||||
def read_value(self):
|
||||
return self._dev.value # mapping is done by datatype upon export()
|
||||
|
||||
def read_status(self):
|
||||
status = self.read_status()
|
||||
self.setFastPoll(self.isBusy(status))
|
||||
return status
|
||||
|
||||
def write_target(self, value):
|
||||
self._dev.value = value
|
||||
self.read_value()
|
||||
self.read_status() # this will also set fast poll
|
||||
return self.read_target()
|
||||
|
||||
def read_target(self):
|
||||
attrObj = self._dev.read_attribute('value')
|
||||
@ -845,22 +876,25 @@ class NamedDigitalOutput(DigitalOutput):
|
||||
datatype=StringType(), export=False)
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalOutput, self).initModule()
|
||||
super().initModule()
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
mapping = self.mapping
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(mapping)
|
||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||
self.accessibles['target'].setProperty('datatype', EnumType('target', **mapping))
|
||||
except Exception as e:
|
||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
||||
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||
|
||||
def write_target(self, value):
|
||||
# map from enum-str to integer value
|
||||
self._dev.value = int(value)
|
||||
self.read_value()
|
||||
return self.read_target()
|
||||
|
||||
|
||||
class PartialDigitalOutput(NamedDigitalOutput):
|
||||
@ -875,7 +909,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalOutput, self).initModule()
|
||||
super().initModule()
|
||||
self._mask = (1 << self.bitwidth) - 1
|
||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||
# self.accessibles['target'].datatype = IntRange(0, self._mask)
|
||||
@ -891,6 +925,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
(value << self.startbit)
|
||||
self._dev.value = newvalue
|
||||
self.read_value()
|
||||
return self.read_target()
|
||||
|
||||
|
||||
class StringIO(PyTangoDevice, Module):
|
||||
|
78
test/test_attach.py
Normal file
78
test/test_attach.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- 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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from secop.modules import Module, Attached
|
||||
from secop.protocol.dispatcher import Dispatcher
|
||||
|
||||
|
||||
# class DispatcherStub:
|
||||
# # omit_unchanged_within = 0
|
||||
#
|
||||
# # def __init__(self, updates):
|
||||
# # self.updates = updates
|
||||
# #
|
||||
# # def announce_update(self, modulename, pname, pobj):
|
||||
# # self.updates.setdefault(modulename, {})
|
||||
# # if pobj.readerror:
|
||||
# # self.updates[modulename]['error', pname] = str(pobj.readerror)
|
||||
# # else:
|
||||
# # self.updates[modulename][pname] = pobj.value
|
||||
#
|
||||
# def __init__(self):
|
||||
# self.modules = {}
|
||||
#
|
||||
# def get_module(self, name):
|
||||
# return self.modules[name]
|
||||
#
|
||||
# def register_module(self, name, module):
|
||||
# self.modules[name] = module
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
logger = LoggerStub()
|
||||
|
||||
|
||||
class ServerStub:
|
||||
restart = None
|
||||
shutdown = None
|
||||
|
||||
def __init__(self):
|
||||
self.dispatcher = Dispatcher('dispatcher', logger, {}, self)
|
||||
|
||||
|
||||
def test_attach():
|
||||
class Mod(Module):
|
||||
att = Attached()
|
||||
|
||||
srv = ServerStub()
|
||||
a = Module('a', logger, {'description': ''}, srv)
|
||||
m = Mod('m', logger, {'description': '', 'att': 'a'}, srv)
|
||||
assert m.propertyValues['att'] == 'a'
|
||||
srv.dispatcher.register_module(a, 'a')
|
||||
srv.dispatcher.register_module(m, 'm')
|
||||
assert m.att == a
|
@ -28,7 +28,9 @@ import pytest
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
||||
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
||||
StringType, StructOf, TextType, TupleOf, get_datatype
|
||||
StringType, StructOf, TextType, TupleOf, get_datatype, \
|
||||
DiscouragedConversion
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
def copytest(dt):
|
||||
@ -36,6 +38,7 @@ def copytest(dt):
|
||||
assert dt.export_datatype() == dt.copy().export_datatype()
|
||||
assert dt != dt.copy()
|
||||
|
||||
|
||||
def test_DataType():
|
||||
dt = DataType()
|
||||
with pytest.raises(NotImplementedError):
|
||||
@ -116,7 +119,6 @@ def test_IntRange():
|
||||
dt('1.3')
|
||||
dt(1)
|
||||
dt(0)
|
||||
dt('1')
|
||||
with pytest.raises(ProgrammingError):
|
||||
IntRange('xc', 'Yx')
|
||||
|
||||
@ -132,6 +134,7 @@ def test_IntRange():
|
||||
with pytest.raises(ConfigError):
|
||||
dt.checkProperties()
|
||||
|
||||
|
||||
def test_ScaledInteger():
|
||||
dt = ScaledInteger(0.01, -3, 3)
|
||||
copytest(dt)
|
||||
@ -407,6 +410,7 @@ def test_ArrayOf():
|
||||
dt = ArrayOf(EnumType('myenum', single=0), 5)
|
||||
copytest(dt)
|
||||
|
||||
|
||||
def test_TupleOf():
|
||||
# test constructor catching illegal arguments
|
||||
with pytest.raises(ValueError):
|
||||
@ -641,6 +645,7 @@ def test_oneway_compatible(dt, contained_in):
|
||||
with pytest.raises(ValueError):
|
||||
contained_in.compatible(dt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt1, dt2', [
|
||||
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
|
||||
(IntRange(0,1), BoolType()),
|
||||
@ -650,6 +655,7 @@ def test_twoway_compatible(dt1, dt2):
|
||||
dt1.compatible(dt1)
|
||||
dt2.compatible(dt2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt1, dt2', [
|
||||
(StringType(), FloatRange()),
|
||||
(IntRange(-10, 10), StringType()),
|
||||
@ -665,3 +671,12 @@ def test_incompatible(dt1, dt2):
|
||||
dt1.compatible(dt2)
|
||||
with pytest.raises(ValueError):
|
||||
dt2.compatible(dt1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt', [FloatRange(), IntRange(), ScaledInteger(1)])
|
||||
def test_lazy_validation(dt):
|
||||
generalConfig.defaults['lazy_number_validation'] = True
|
||||
dt('0')
|
||||
generalConfig.defaults['lazy_number_validation'] = False
|
||||
with pytest.raises(DiscouragedConversion):
|
||||
dt('0')
|
||||
|
234
test/test_handler.py
Normal file
234
test/test_handler.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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from secop.rwhandler import ReadHandler, WriteHandler, \
|
||||
CommonReadHandler, CommonWriteHandler, nopoll
|
||||
from secop.core import Module, Parameter, FloatRange, Done
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
# the first update from the poller comes a very short time after the
|
||||
# initial value from the timestamp. However, in the test below
|
||||
# the second update happens after the updates dict is cleared
|
||||
# -> we have to inhibit the 'omit unchanged update' feature
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, updates):
|
||||
self.updates = updates
|
||||
|
||||
def announce_update(self, modulename, pname, pobj):
|
||||
self.updates.setdefault(modulename, {})
|
||||
if pobj.readerror:
|
||||
self.updates[modulename]['error', pname] = str(pobj.readerror)
|
||||
else:
|
||||
self.updates[modulename][pname] = pobj.value
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
logger = LoggerStub()
|
||||
|
||||
|
||||
class ServerStub:
|
||||
def __init__(self, updates):
|
||||
self.dispatcher = DispatcherStub(updates)
|
||||
|
||||
|
||||
class ModuleTest(Module):
|
||||
def __init__(self, updates=None, **opts):
|
||||
opts['description'] = ''
|
||||
super().__init__('mod', logger, opts, ServerStub(updates or {}))
|
||||
|
||||
|
||||
def test_handler():
|
||||
data = []
|
||||
|
||||
class Mod(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@ReadHandler(['a', 'b'])
|
||||
def read_hdl(self, pname):
|
||||
value = data.pop()
|
||||
data.append(pname)
|
||||
return value
|
||||
|
||||
@WriteHandler(['a', 'b'])
|
||||
def write_hdl(self, pname, value):
|
||||
data.append(pname)
|
||||
return value
|
||||
|
||||
assert Mod.read_a.poll is True
|
||||
assert Mod.read_b.poll is True
|
||||
|
||||
m = Mod()
|
||||
|
||||
data.append(1.2)
|
||||
assert m.read_a() == 1.2
|
||||
assert data.pop() == 'a'
|
||||
|
||||
data.append(1.3)
|
||||
assert m.read_b() == 1.3
|
||||
assert data.pop() == 'b'
|
||||
|
||||
assert m.write_a(1.5) == 1.5
|
||||
assert m.a == 1.5
|
||||
assert data.pop() == 'a'
|
||||
|
||||
assert m.write_b(7) == 7
|
||||
assert m.b == 7
|
||||
assert data.pop() == 'b'
|
||||
|
||||
data.append(Done)
|
||||
assert m.read_b() == 7
|
||||
assert data.pop() == 'b'
|
||||
|
||||
assert data == []
|
||||
|
||||
|
||||
def test_common_handler():
|
||||
data = []
|
||||
|
||||
class Mod(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@CommonReadHandler(['a', 'b'])
|
||||
def read_hdl(self):
|
||||
self.a, self.b = data.pop()
|
||||
data.append('read_hdl')
|
||||
|
||||
@CommonWriteHandler(['a', 'b'])
|
||||
def write_hdl(self, values):
|
||||
self.a = values['a']
|
||||
self.b = values['b']
|
||||
data.append('write_hdl')
|
||||
|
||||
assert set([Mod.read_a.poll, Mod.read_b.poll]) == {True, False}
|
||||
|
||||
m = Mod(a=1, b=2)
|
||||
assert m.writeDict == {'a': 1, 'b': 2}
|
||||
m.write_a(3)
|
||||
assert m.a == 3
|
||||
assert m.b == 2
|
||||
assert data.pop() == 'write_hdl'
|
||||
assert m.writeDict == {}
|
||||
|
||||
m.write_b(4)
|
||||
assert m.a == 3
|
||||
assert m.b == 4
|
||||
assert data.pop() == 'write_hdl'
|
||||
|
||||
data.append((3, 4))
|
||||
assert m.read_a() == 3
|
||||
assert m.a == 3
|
||||
assert m.b == 4
|
||||
assert data.pop() == 'read_hdl'
|
||||
data.append((5, 6))
|
||||
assert m.read_b() == 6
|
||||
assert data.pop() == 'read_hdl'
|
||||
|
||||
data.append((1.1, 2.2))
|
||||
assert m.read_b() == 2.2
|
||||
assert m.a == 1.1
|
||||
assert m.b == 2.2
|
||||
assert data.pop() == 'read_hdl'
|
||||
|
||||
assert data == []
|
||||
|
||||
|
||||
def test_nopoll():
|
||||
class Mod1(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@ReadHandler(['a', 'b'])
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod1.read_a.poll is True
|
||||
assert Mod1.read_b.poll is True
|
||||
|
||||
class Mod2(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@CommonReadHandler(['a', 'b'])
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod2.read_a.poll is True
|
||||
assert Mod2.read_b.poll is False
|
||||
|
||||
class Mod3(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@ReadHandler(['a', 'b'])
|
||||
@nopoll
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod3.read_a.poll is False
|
||||
assert Mod3.read_b.poll is False
|
||||
|
||||
class Mod4(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@nopoll
|
||||
@ReadHandler(['a', 'b'])
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod4.read_a.poll is False
|
||||
assert Mod4.read_b.poll is False
|
||||
|
||||
class Mod5(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@CommonReadHandler(['a', 'b'])
|
||||
@nopoll
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod5.read_a.poll is False
|
||||
assert Mod5.read_b.poll is False
|
||||
|
||||
class Mod6(ModuleTest):
|
||||
a = Parameter('', FloatRange(), readonly=False)
|
||||
b = Parameter('', FloatRange(), readonly=False)
|
||||
|
||||
@nopoll
|
||||
@CommonReadHandler(['a', 'b'])
|
||||
def read_hdl(self):
|
||||
pass
|
||||
|
||||
assert Mod6.read_a.poll is False
|
||||
assert Mod6.read_b.poll is False
|
@ -120,7 +120,7 @@ def test_IOHandler():
|
||||
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
||||
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||
|
||||
def sendRecv(self, command):
|
||||
def communicate(self, command):
|
||||
assert data.pop('command') == command
|
||||
return data.pop('reply')
|
||||
|
||||
@ -146,7 +146,7 @@ def test_IOHandler():
|
||||
print(updates)
|
||||
updates.clear() # get rid of updates from initialisation
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'SIMPLE?')
|
||||
data.push('reply', '4.51')
|
||||
# for analyze_group1
|
||||
@ -159,7 +159,7 @@ def test_IOHandler():
|
||||
assert updates.pop('simple') == 45.1
|
||||
assert not updates
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD?3')
|
||||
data.push('reply', '1.23,text,5')
|
||||
# for analyze_group2
|
||||
@ -172,7 +172,7 @@ def test_IOHandler():
|
||||
assert data.empty()
|
||||
assert not updates
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD?3')
|
||||
data.push('reply', '1.23,text,5')
|
||||
# for analyze_group2
|
||||
@ -183,7 +183,7 @@ def test_IOHandler():
|
||||
data.push('self', 12.3, 'string')
|
||||
data.push('new', 12.3, 'FOO')
|
||||
data.push('changed', 1.23, 'foo', 9)
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
|
||||
data.push('reply', '1.23,foo,9')
|
||||
# for analyze_group2
|
||||
|
274
test/test_logging.py
Normal file
274
test/test_logging.py
Normal file
@ -0,0 +1,274 @@
|
||||
# -*- 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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import pytest
|
||||
import mlzlog
|
||||
from secop.modules import Module
|
||||
from secop.protocol.dispatcher import Dispatcher
|
||||
from secop.protocol.interface import encode_msg_frame, decode_msg
|
||||
import secop.logging
|
||||
from secop.logging import logger, generalConfig, HasComlog
|
||||
|
||||
|
||||
class ServerStub:
|
||||
restart = None
|
||||
shutdown = None
|
||||
|
||||
def __init__(self):
|
||||
self.dispatcher = Dispatcher('', logger.log.getChild('dispatcher'), {}, self)
|
||||
|
||||
|
||||
class Connection:
|
||||
def __init__(self, name, dispatcher, result):
|
||||
self.result = result
|
||||
self.dispatcher = dispatcher
|
||||
self.name = name
|
||||
dispatcher.add_connection(self)
|
||||
|
||||
def send_reply(self, msg):
|
||||
self.result.append(encode_msg_frame(*msg).strip().decode())
|
||||
|
||||
def send(self, msg):
|
||||
request = decode_msg(msg.encode())
|
||||
assert self.dispatcher.handle_request(self, request) == request
|
||||
|
||||
|
||||
@pytest.fixture(name='init')
|
||||
def init_(monkeypatch):
|
||||
logger.__init__()
|
||||
|
||||
class Playground:
|
||||
def __init__(self, console_level='debug', comlog=True, com_module=True):
|
||||
self.result_dict = result_dict = dict(
|
||||
console=[], comlog=[], conn1=[], conn2=[])
|
||||
|
||||
class ConsoleHandler(mlzlog.Handler):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__()
|
||||
self.result = result_dict['console']
|
||||
|
||||
def emit(self, record):
|
||||
if record.name != 'frappy.dispatcher':
|
||||
self.result.append('%s %s %s' % (record.name, record.levelname, record.getMessage()))
|
||||
|
||||
class ComLogHandler(mlzlog.Handler):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__()
|
||||
self.result = result_dict['comlog']
|
||||
|
||||
def emit(self, record):
|
||||
self.result.append('%s %s' % (record.name.split('.')[1], record.getMessage()))
|
||||
|
||||
class LogfileHandler(mlzlog.Handler):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__()
|
||||
|
||||
def noop(self, *args):
|
||||
pass
|
||||
|
||||
close = flush = emit = noop
|
||||
|
||||
monkeypatch.setattr(mlzlog, 'ColoredConsoleHandler', ConsoleHandler)
|
||||
monkeypatch.setattr(secop.logging, 'ComLogfileHandler', ComLogHandler)
|
||||
monkeypatch.setattr(secop.logging, 'LogfileHandler', LogfileHandler)
|
||||
|
||||
class Mod(Module):
|
||||
result = []
|
||||
|
||||
def __init__(self, name, srv, **kwds):
|
||||
kwds['description'] = ''
|
||||
super().__init__(name or 'mod', logger.log.getChild(name), kwds, srv)
|
||||
srv.dispatcher.register_module(self, name, name)
|
||||
self.result[:] = []
|
||||
|
||||
def earlyInit(self):
|
||||
pass
|
||||
|
||||
class Com(HasComlog, Mod):
|
||||
def __init__(self, name, srv, **kwds):
|
||||
super().__init__(name, srv, **kwds)
|
||||
self.earlyInit()
|
||||
self.log.handlers[-1].result = result_dict['comlog']
|
||||
|
||||
def communicate(self, request):
|
||||
self.comLog('> %s', request)
|
||||
|
||||
generalConfig.testinit(logger_root='frappy', comlog=comlog)
|
||||
logger.init(console_level)
|
||||
self.srv = ServerStub()
|
||||
|
||||
self.conn1 = Connection('conn1', self.srv.dispatcher, self.result_dict['conn1'])
|
||||
self.conn2 = Connection('conn2', self.srv.dispatcher, self.result_dict['conn2'])
|
||||
self.mod = Mod('mod', self.srv)
|
||||
self.com = Com('com', self.srv, comlog=com_module)
|
||||
for item in self.result_dict.values():
|
||||
assert item == []
|
||||
|
||||
def check(self, both=None, **expected):
|
||||
if both:
|
||||
expected['conn1'] = expected['conn2'] = both
|
||||
assert self.result_dict['console'] == expected.get('console', [])
|
||||
assert self.result_dict['comlog'] == expected.get('comlog', [])
|
||||
assert self.result_dict['conn1'] == expected.get('conn1', [])
|
||||
assert self.result_dict['conn2'] == expected.get('conn2', [])
|
||||
for item in self.result_dict.values():
|
||||
item[:] = []
|
||||
|
||||
def comlog(self, flag):
|
||||
logger.comlog = flag
|
||||
|
||||
yield Playground
|
||||
# revert settings
|
||||
generalConfig.testinit()
|
||||
logger.__init__()
|
||||
|
||||
|
||||
def test_mod_info(init):
|
||||
p = init()
|
||||
p.mod.log.info('i')
|
||||
p.check(console=['frappy.mod INFO i'])
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.mod.log.info('i')
|
||||
p.check(console=['frappy.mod INFO i'], both=['log mod:info "i"'])
|
||||
|
||||
|
||||
def test_mod_debug(init):
|
||||
p = init()
|
||||
p.mod.log.debug('d')
|
||||
p.check(console=['frappy.mod DEBUG d'])
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.mod.log.debug('d')
|
||||
p.check(console=['frappy.mod DEBUG d'], conn1=['log mod:debug "d"'])
|
||||
|
||||
|
||||
def test_com_info(init):
|
||||
p = init()
|
||||
p.com.log.info('i')
|
||||
p.check(console=['frappy.com INFO i'])
|
||||
p.conn1.send('logging com "info"')
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.com.log.info('i')
|
||||
p.check(console=['frappy.com INFO i'], both=['log com:info "i"'])
|
||||
|
||||
|
||||
def test_com_debug(init):
|
||||
p = init()
|
||||
p.com.log.debug('d')
|
||||
p.check(console=['frappy.com DEBUG d'])
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.com.log.debug('d')
|
||||
p.check(console=['frappy.com DEBUG d'], conn2=['log com:debug "d"'])
|
||||
|
||||
|
||||
def test_com_com(init):
|
||||
p = init()
|
||||
p.com.communicate('x')
|
||||
p.check(console=['frappy.com COMLOG > x'], comlog=['com > x'])
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.com.communicate('x')
|
||||
p.check(console=['frappy.com COMLOG > x'], comlog=['com > x'], conn2=['log com:comlog "> x"'])
|
||||
|
||||
|
||||
def test_main_info(init):
|
||||
p = init(console_level='info')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.check(comlog=['com > x'])
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.com.communicate('x')
|
||||
p.check(comlog=['com > x'], conn2=['log com:comlog "> x"'])
|
||||
|
||||
|
||||
def test_comlog_off(init):
|
||||
p = init(console_level='info', comlog=False)
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.check()
|
||||
|
||||
|
||||
def test_comlog_module_off(init):
|
||||
p = init(console_level='info', com_module=False)
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.check()
|
||||
|
||||
|
||||
def test_remote_all_off(init):
|
||||
p = init()
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks = dict(
|
||||
console=['frappy.mod DEBUG d', 'frappy.com COMLOG > x', 'frappy.mod INFO i'],
|
||||
comlog=['com > x'],
|
||||
conn1=['log mod:debug "d"', 'log mod:info "i"'],
|
||||
conn2=['log com:comlog "> x"', 'log mod:info "i"'])
|
||||
p.check(**checks)
|
||||
p.conn1.send('logging "off"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks.pop('conn1')
|
||||
p.check(**checks)
|
||||
p.conn2.send('logging . "off"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks.pop('conn2')
|
||||
p.check(**checks)
|
||||
|
||||
|
||||
def test_remote_single_off(init):
|
||||
p = init()
|
||||
p.conn1.send('logging mod "debug"')
|
||||
p.conn2.send('logging mod "info"')
|
||||
p.conn2.send('logging com "debug"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks = dict(
|
||||
console=['frappy.mod DEBUG d', 'frappy.com COMLOG > x', 'frappy.mod INFO i'],
|
||||
comlog=['com > x'],
|
||||
conn1=['log mod:debug "d"', 'log mod:info "i"'],
|
||||
conn2=['log com:comlog "> x"', 'log mod:info "i"'])
|
||||
p.check(**checks)
|
||||
p.conn2.send('logging com "off"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks['conn2'] = ['log mod:info "i"']
|
||||
p.check(**checks)
|
||||
p.conn2.send('logging mod "off"')
|
||||
p.mod.log.debug('d')
|
||||
p.com.communicate('x')
|
||||
p.mod.log.info('i')
|
||||
checks['conn2'] = []
|
||||
p.check(**checks)
|
@ -22,15 +22,16 @@
|
||||
# *****************************************************************************
|
||||
"""test data types."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, FloatRange, StringType, IntRange
|
||||
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||
from secop.errors import ProgrammingError, ConfigError
|
||||
from secop.modules import Communicator, Drivable, Readable, Module
|
||||
from secop.params import Command, Parameter
|
||||
from secop.poller import BasicPoller
|
||||
from secop.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
@ -52,9 +53,10 @@ class DispatcherStub:
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, *args):
|
||||
print(*args)
|
||||
info = warning = exception = debug
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
logger = LoggerStub()
|
||||
@ -65,13 +67,21 @@ class ServerStub:
|
||||
self.dispatcher = DispatcherStub(updates)
|
||||
|
||||
|
||||
class DummyMultiEvent(threading.Event):
|
||||
def get_trigger(self):
|
||||
def trigger(event=self):
|
||||
event.set()
|
||||
sys.exit()
|
||||
return trigger
|
||||
|
||||
|
||||
def test_Communicator():
|
||||
o = Communicator('communicator', LoggerStub(), {'.description':''}, ServerStub({}))
|
||||
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
|
||||
o.earlyInit()
|
||||
o.initModule()
|
||||
event = threading.Event()
|
||||
o.startModule(event.set)
|
||||
assert event.is_set() # event should be set immediately
|
||||
event = DummyMultiEvent()
|
||||
o.startModule(event)
|
||||
assert event.is_set() # event should be set immediately
|
||||
|
||||
|
||||
def test_ModuleMagic():
|
||||
@ -87,14 +97,13 @@ def test_ModuleMagic():
|
||||
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||
value = Parameter(datatype=StringType(), default='first')
|
||||
target = Parameter(datatype=StringType(), default='')
|
||||
|
||||
@Command(argument=BoolType(), result=BoolType())
|
||||
def cmd2(self, arg):
|
||||
"""another stuff"""
|
||||
return not arg
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def read_param1(self):
|
||||
return True
|
||||
|
||||
@ -104,12 +113,16 @@ def test_ModuleMagic():
|
||||
def read_a1(self):
|
||||
return True
|
||||
|
||||
@nopoll
|
||||
def read_a2(self):
|
||||
return True
|
||||
|
||||
def read_value(self):
|
||||
return 'second'
|
||||
|
||||
def read_status(self):
|
||||
return 'IDLE', 'ok'
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
def do_this(self): # old style command
|
||||
@ -132,9 +145,12 @@ def test_ModuleMagic():
|
||||
return arg
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||
target = Parameter(datatype=FloatRange(), default=0)
|
||||
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
||||
# remark: it might be a programming error to override the datatype
|
||||
# and not overriding the read_* method. This is not checked!
|
||||
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
||||
poll=True, readonly=False, initwrite=True)
|
||||
readonly=False, initwrite=True)
|
||||
|
||||
def write_a1(self, value):
|
||||
self._a1_written = value
|
||||
@ -170,30 +186,33 @@ def test_ModuleMagic():
|
||||
|
||||
# check for inital updates working properly
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
expectedBeforeStart = {'target': 0.0, 'status': (Drivable.Status.IDLE, ''),
|
||||
expectedBeforeStart = {'target': '', 'status': (Drivable.Status.IDLE, ''),
|
||||
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
|
||||
'value': 'first'}
|
||||
assert updates.pop('o1') == expectedBeforeStart
|
||||
o1.earlyInit()
|
||||
event = threading.Event()
|
||||
o1.startModule(event.set)
|
||||
event = DummyMultiEvent()
|
||||
o1.startModule(event)
|
||||
event.wait()
|
||||
# should contain polled values
|
||||
expectedAfterStart = {'status': (Drivable.Status.IDLE, ''),
|
||||
'value': 'second'}
|
||||
expectedAfterStart = {
|
||||
'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
|
||||
'param1': True, 'param2': 0.0, 'a1': True}
|
||||
assert updates.pop('o1') == expectedAfterStart
|
||||
|
||||
# check in addition if parameters are written
|
||||
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
|
||||
# no update for b2, as this has to be written
|
||||
expectedBeforeStart['a1'] = 2.7
|
||||
expectedBeforeStart['target'] = 0.0
|
||||
assert updates.pop('o2') == expectedBeforeStart
|
||||
o2.earlyInit()
|
||||
event = threading.Event()
|
||||
o2.startModule(event.set)
|
||||
event = DummyMultiEvent()
|
||||
o2.startModule(event)
|
||||
event.wait()
|
||||
# value has changed type, b2 and a1 are written
|
||||
expectedAfterStart.update(value=0, b2=True, a1=2.7)
|
||||
expectedAfterStart.update(value=0, b2=True, a1=True)
|
||||
# ramerk: a1=True: this behaviour is a Porgamming error
|
||||
assert updates.pop('o2') == expectedAfterStart
|
||||
assert o2._a1_written == 2.7
|
||||
assert o2._b2_written is True
|
||||
@ -210,13 +229,15 @@ def test_ModuleMagic():
|
||||
# check '$' in unit works properly
|
||||
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||
cfg = Newclass2.configurables
|
||||
assert set(cfg.keys()) == {'export', 'group', 'description',
|
||||
assert set(cfg.keys()) == {
|
||||
'export', 'group', 'description', 'disable_value_range_check',
|
||||
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
|
||||
'a1'}
|
||||
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
|
||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
|
||||
'cmd2', 'value', 'a1'}
|
||||
assert set(cfg['value'].keys()) == {
|
||||
'group', 'export', 'relative_resolution',
|
||||
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
|
||||
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
|
||||
'absolute_resolution', 'max', 'min', 'readonly', 'constant',
|
||||
'description', 'needscfg'}
|
||||
|
||||
# check on the level of classes
|
||||
@ -459,3 +480,177 @@ def test_command_none():
|
||||
|
||||
assert 'stop' in Mod('o', logger, {'description': ''}, srv).accessibles
|
||||
assert 'stop' not in Mod2('o', logger, {'description': ''}, srv).accessibles
|
||||
|
||||
|
||||
def test_bad_method():
|
||||
class Mod0(Drivable): # pylint: disable=unused-variable
|
||||
def write_target(self, value):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Drivable): # pylint: disable=unused-variable
|
||||
def write_taget(self, value):
|
||||
pass
|
||||
|
||||
class Mod2(Drivable): # pylint: disable=unused-variable
|
||||
def read_value(self, value):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Drivable): # pylint: disable=unused-variable
|
||||
def read_valu(self, value):
|
||||
pass
|
||||
|
||||
|
||||
def test_generic_access():
|
||||
class Mod(Module):
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
unhandled = Parameter('unhandled param', StringType(), default='', readonly=False)
|
||||
data = {'param': ''}
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def read_handler(self, pname):
|
||||
value = self.data[pname]
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def write_handler(self, pname, value):
|
||||
value = value.lower()
|
||||
self.data[pname] = value
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'param': 'initial value'}, srv)
|
||||
assert obj.param == 'initial value'
|
||||
assert obj.write_param('Cheese') == 'cheese'
|
||||
assert obj.write_unhandled('Cheese') == 'Cheese'
|
||||
assert updates == {'obj': {'param': 'cheese', 'unhandled': 'Cheese'}}
|
||||
updates.clear()
|
||||
assert obj.write_param('Potato') == 'potato'
|
||||
assert updates == {'obj': {'param': 'potato'}}
|
||||
updates.clear()
|
||||
assert obj.read_param() == 'potato'
|
||||
assert obj.read_unhandled()
|
||||
assert updates == {'obj': {'param': 'potato'}}
|
||||
updates.clear()
|
||||
assert updates == {}
|
||||
|
||||
|
||||
def test_duplicate_handler_name():
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def handler(self, pname):
|
||||
pass
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def handler(self, pname, value): # pylint: disable=function-redefined
|
||||
pass
|
||||
|
||||
|
||||
def test_handler_overwrites_method():
|
||||
with pytest.raises(RuntimeError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def read_handler(self, pname):
|
||||
pass
|
||||
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
class Mod2(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def write_handler(self, pname, value):
|
||||
pass
|
||||
|
||||
def write_param(self, value):
|
||||
pass
|
||||
|
||||
|
||||
def test_no_read_write():
|
||||
class Mod(Module):
|
||||
param = Parameter('test param', StringType(), readonly=False)
|
||||
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'param': 'cheese'}, srv)
|
||||
assert obj.param == 'cheese'
|
||||
assert obj.read_param() == 'cheese'
|
||||
assert updates == {'obj': {'param': 'cheese'}}
|
||||
assert obj.write_param('egg') == 'egg'
|
||||
assert obj.param == 'egg'
|
||||
assert updates == {'obj': {'param': 'egg'}}
|
||||
|
||||
|
||||
def test_incompatible_value_target():
|
||||
class Mod1(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 11), default=0)
|
||||
|
||||
class Mod2(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', StringType(), default='')
|
||||
|
||||
class Mod3(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', ScaledInteger(1, 0, 10), default=0)
|
||||
|
||||
srv = ServerStub({})
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod1('obj', logger, {'description': ''}, srv) # pylint: disable=unused-variable
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||
|
||||
obj = Mod3('obj', logger, {'description': ''}, srv)
|
||||
|
||||
|
||||
def test_problematic_value_range():
|
||||
class Mod(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 10), default=0)
|
||||
|
||||
srv = ServerStub({})
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'value.max': 10.1}, srv) # pylint: disable=unused-variable
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod('obj', logger, {'description': ''}, srv)
|
||||
|
||||
class Mod2(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', FloatRange(), default=0)
|
||||
|
||||
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||
obj = Mod2('obj', logger, {'description': '', 'target.min': 0, 'target.max': 10}, srv)
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod('obj', logger, {
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
||||
obj = Mod('obj', logger, {'disable_value_range_check': True,
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
||||
generalConfig.defaults['disable_value_range_check'] = True
|
||||
|
||||
class Mod4(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 10), default=0)
|
||||
obj = Mod4('obj', logger, {
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
60
test/test_multievent.py
Normal file
60
test/test_multievent.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- 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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import time
|
||||
from secop.lib.multievent import MultiEvent
|
||||
|
||||
|
||||
def test_without_timeout():
|
||||
m = MultiEvent()
|
||||
s1 = m.get_trigger(name='s1')
|
||||
s2 = m.get_trigger(name='s2')
|
||||
assert not m.wait(0)
|
||||
assert m.deadline() is None
|
||||
assert m.waiting_for() == {'s1', 's2'}
|
||||
s2()
|
||||
assert m.waiting_for() == {'s1'}
|
||||
assert not m.wait(0)
|
||||
s1()
|
||||
assert not m.waiting_for()
|
||||
assert m.wait(0)
|
||||
|
||||
|
||||
def test_with_timeout(monkeypatch):
|
||||
current_time = 1000
|
||||
monkeypatch.setattr(time, 'monotonic', lambda: current_time)
|
||||
m = MultiEvent()
|
||||
assert m.deadline() == 0
|
||||
m.name = 's1'
|
||||
s1 = m.get_trigger(10)
|
||||
assert m.deadline() == 1010
|
||||
m.name = 's2'
|
||||
s2 = m.get_trigger(20)
|
||||
assert m.deadline() == 1020
|
||||
current_time += 21
|
||||
assert not m.wait(0)
|
||||
assert m.waiting_for() == {'s1', 's2'}
|
||||
s1()
|
||||
assert m.waiting_for() == {'s2'}
|
||||
s2()
|
||||
assert not m.waiting_for()
|
||||
assert m.wait(0)
|
@ -21,18 +21,20 @@
|
||||
# *****************************************************************************
|
||||
"""test poller."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.modules import Drivable
|
||||
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
|
||||
from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
|
||||
from secop.lib.multievent import MultiEvent
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
class Time:
|
||||
STARTTIME = 1000 # artificial time zero
|
||||
STARTTIME = 1000 # artificial time zero
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
self.finish = float('inf')
|
||||
@ -61,187 +63,103 @@ class Time:
|
||||
self.seconds += seconds
|
||||
self.busytime += seconds
|
||||
|
||||
|
||||
artime = Time() # artificial test time
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_time(monkeypatch):
|
||||
monkeypatch.setattr(time, 'time', artime.time)
|
||||
|
||||
class Event(threading.Event):
|
||||
def wait(self, timeout=None):
|
||||
artime.sleep(max(0, timeout))
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self):
|
||||
self.flag = False
|
||||
class DispatcherStub:
|
||||
maxcycles = 10
|
||||
|
||||
def wait(self, timeout):
|
||||
artime.sleep(max(0,timeout))
|
||||
|
||||
def set(self):
|
||||
self.flag = True
|
||||
|
||||
def clear(self):
|
||||
self.flag = False
|
||||
|
||||
def is_set(self):
|
||||
return self.flag
|
||||
|
||||
|
||||
class Parameter:
|
||||
def __init__(self, name, readonly, poll, polltype, interval):
|
||||
self.poll = poll
|
||||
self.polltype = polltype # used for check only
|
||||
self.export = name
|
||||
self.readonly = readonly
|
||||
self.interval = interval
|
||||
self.timestamp = 0
|
||||
self.handler = None
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.cnt = 0
|
||||
self.span = 0
|
||||
self.maxspan = 0
|
||||
|
||||
def rfunc(self):
|
||||
artime.busy(artime.commtime)
|
||||
def announce_update(self, modulename, pname, pobj):
|
||||
now = artime.time()
|
||||
self.span = now - self.timestamp
|
||||
self.maxspan = max(self.maxspan, self.span)
|
||||
self.timestamp = now
|
||||
self.cnt += 1
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items())
|
||||
|
||||
|
||||
class Module:
|
||||
properties = {}
|
||||
pollerClass = Poller
|
||||
iodev = 'common_iodev'
|
||||
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
|
||||
counts=(), auto=None):
|
||||
'''create a dummy module
|
||||
|
||||
nauto, ndynamic, nregular, nslow are the number of parameters of each polltype
|
||||
'''
|
||||
self.pollinterval = pollinterval
|
||||
self.fast_pollfactor = fastfactor
|
||||
self.slow_pollfactor = slowfactor
|
||||
self.parameters = OrderedDict()
|
||||
self.name = name
|
||||
self.is_busy = busy
|
||||
if auto is not None:
|
||||
self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC)
|
||||
# readonly = False should not matter:
|
||||
self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC)
|
||||
self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR)
|
||||
self.pslow = self.addPar('slow', False, auto or SLOW, SLOW)
|
||||
self.addPar('notpolled', True, False, 0)
|
||||
self.counts = 'auto'
|
||||
if hasattr(pobj, 'stat'):
|
||||
pobj.stat.append(now)
|
||||
else:
|
||||
ndynamic, nregular, nslow = counts
|
||||
for i in range(ndynamic):
|
||||
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC)
|
||||
for i in range(nregular):
|
||||
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR)
|
||||
for i in range(nslow):
|
||||
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
|
||||
self.counts = counts
|
||||
pobj.stat = [now]
|
||||
self.maxcycles -= 1
|
||||
if self.maxcycles <= 0:
|
||||
self.finish_event.set()
|
||||
sys.exit() # stop thread
|
||||
|
||||
def addPar(self, name, readonly, poll, expected_polltype):
|
||||
# self.count[polltype] += 1
|
||||
expected_interval = self.pollinterval
|
||||
if expected_polltype == SLOW:
|
||||
expected_interval *= self.slow_pollfactor
|
||||
elif expected_polltype == DYNAMIC and self.is_busy:
|
||||
expected_interval *= self.fast_pollfactor
|
||||
pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval)
|
||||
setattr(self, 'read_' + pobj.export, pobj.rfunc)
|
||||
self.parameters[pobj.export] = pobj
|
||||
return pobj
|
||||
|
||||
def isBusy(self):
|
||||
return self.is_busy
|
||||
class ServerStub:
|
||||
def __init__(self):
|
||||
self.dispatcher = DispatcherStub()
|
||||
|
||||
def pollOneParam(self, pname):
|
||||
getattr(self, 'read_' + pname)()
|
||||
|
||||
def writeInitParams(self):
|
||||
pass
|
||||
class Base(Module):
|
||||
def __init__(self):
|
||||
srv = ServerStub()
|
||||
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
|
||||
self.dispatcher = srv.dispatcher
|
||||
self.nextPollEvent = Event()
|
||||
|
||||
def __repr__(self):
|
||||
rdict = self.__dict__.copy()
|
||||
rdict.pop('parameters')
|
||||
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name,
|
||||
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1),
|
||||
self.pollinterval, self.is_busy)
|
||||
def run(self, maxcycles):
|
||||
self.dispatcher.maxcycles = maxcycles
|
||||
self.dispatcher.finish_event = threading.Event()
|
||||
self.startModule(MultiEvent())
|
||||
self.dispatcher.finish_event.wait(1)
|
||||
|
||||
module_list = [
|
||||
[Module('x', 3.0, 0.125, 10, False, auto=True),
|
||||
Module('y', 3.0, 0.125, 10, False, auto=False)],
|
||||
[Module('a', 1.0, 0.25, 4, True, (5, 5, 10)),
|
||||
Module('b', 2.0, 0.25, 4, True, (5, 5, 50))],
|
||||
[Module('c', 1.0, 0.25, 4, False, (5, 0, 0))],
|
||||
[Module('d', 1.0, 0.25, 4, True, (0, 9, 0))],
|
||||
[Module('e', 1.0, 0.25, 4, True, (0, 0, 9))],
|
||||
[Module('f', 1.0, 0.25, 4, True, (0, 0, 0))],
|
||||
]
|
||||
@pytest.mark.parametrize('modules', module_list)
|
||||
def test_Poller(modules):
|
||||
# check for proper timing
|
||||
|
||||
for overloaded in False, True:
|
||||
artime.reset()
|
||||
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
||||
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
||||
pollTable = dict()
|
||||
for module in modules:
|
||||
Poller.add_to_table(pollTable, module)
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval)
|
||||
count[pobj.polltype] += 1
|
||||
pobj.reset()
|
||||
assert len(pollTable) == 1
|
||||
poller = pollTable[(Poller, 'common_iodev')]
|
||||
artime.stop = poller.stop
|
||||
poller._event = Event() # patch Event.wait
|
||||
class Mod1(Base, Readable):
|
||||
param1 = Parameter('', FloatRange())
|
||||
param2 = Parameter('', FloatRange())
|
||||
param3 = Parameter('', FloatRange())
|
||||
param4 = Parameter('', FloatRange())
|
||||
|
||||
assert (sum(count.values()) > 0) == bool(poller)
|
||||
@ReadHandler(('param1', 'param2', 'param3'))
|
||||
def read_param(self, name):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
def started_callback(modules=modules):
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once
|
||||
pobj.reset() # set maxspan and cnt to 0
|
||||
@nopoll
|
||||
def read_param4(self):
|
||||
return 0
|
||||
|
||||
if overloaded:
|
||||
# overloaded scenario
|
||||
artime.commtime = 1.0
|
||||
ncycles = 10
|
||||
if count[SLOW] > 0:
|
||||
cycletime = (count[REGULAR] + 1) * count[SLOW] * 2
|
||||
else:
|
||||
cycletime = max(count[REGULAR], count[DYNAMIC]) * 2
|
||||
artime.reset(cycletime * ncycles * 1.01) # poller will quit given time
|
||||
poller.run(started_callback)
|
||||
total = artime.time() - artime.STARTTIME
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
# average_span = total / (pobj.cnt + 1)
|
||||
assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1)
|
||||
else:
|
||||
# normal scenario
|
||||
artime.commtime = 0.001
|
||||
artime.reset(max(maxspan.values()) * 5) # poller will quit given time
|
||||
poller.run(started_callback)
|
||||
total = artime.time() - artime.STARTTIME
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
assert pobj.cnt > 0
|
||||
assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1
|
||||
assert (pobj.cnt + 1) * pobj.interval >= total * 0.99
|
||||
assert abs(pobj.span - pobj.interval) < 0.01
|
||||
pobj.reset()
|
||||
def read_status(self):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
def read_value(self):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'ncycles, pollinterval, slowinterval, mspan, pspan',
|
||||
[ # normal case: 5+-1 15+-1
|
||||
( 60, 5, 15, (4, 6), (14, 16)),
|
||||
# pollinterval faster then reading: mspan max 3 s (polls of value, status and ONE other parameter)
|
||||
( 60, 1, 5, (1, 3), (5, 16)),
|
||||
])
|
||||
def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch):
|
||||
monkeypatch.setattr(time, 'time', artime.time)
|
||||
artime.reset()
|
||||
m = Mod1()
|
||||
m.pollinterval = pollinterval
|
||||
m.slowInterval = slowinterval
|
||||
m.run(ncycles)
|
||||
assert not hasattr(m.parameters['param4'], 'stat')
|
||||
for pname in ['value', 'status']:
|
||||
pobj = m.parameters[pname]
|
||||
lowcnt = 0
|
||||
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||
if t2 - t1 < mspan[0]:
|
||||
print(t2 - t1)
|
||||
lowcnt += 1
|
||||
assert t2 - t1 <= mspan[1]
|
||||
assert lowcnt <= 1
|
||||
for pname in ['param1', 'param2', 'param3']:
|
||||
pobj = m.parameters[pname]
|
||||
lowcnt = 0
|
||||
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||
if t2 - t1 < pspan[0]:
|
||||
print(pname, t2 - t1)
|
||||
lowcnt += 1
|
||||
assert t2 - t1 <= pspan[1]
|
||||
assert lowcnt <= 1
|
||||
|
@ -26,6 +26,7 @@ import pytest
|
||||
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.core import Parameter
|
||||
|
||||
|
||||
def Prop(*args, name=None, **kwds):
|
||||
@ -38,10 +39,10 @@ V_test_Property = [
|
||||
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||
[Prop(IntRange(), 42, export=True, name='custom', mandatory=True),
|
||||
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='name'),
|
||||
[Prop(IntRange(), 42, export=True, name='name'),
|
||||
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||
@ -85,12 +86,12 @@ def test_Property_basic():
|
||||
Property('')
|
||||
with pytest.raises(ValueError):
|
||||
Property('', 1)
|
||||
Property('', IntRange(), '42', 'extname', False, False)
|
||||
Property('', IntRange(), 42, 'extname', False, False)
|
||||
|
||||
|
||||
def test_Properties():
|
||||
class Cls(HasProperties):
|
||||
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||
aa = Property('', IntRange(0, 99), 42, export=True)
|
||||
bb = Property('', IntRange(), 0, export=False)
|
||||
|
||||
assert Cls.aa.default == 42
|
||||
@ -149,17 +150,25 @@ def test_Property_override():
|
||||
assert o2.a == 3
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
def a(self):
|
||||
pass
|
||||
assert 'collides with' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
class cy(c): # pylint: disable=unused-variable
|
||||
a = 's'
|
||||
|
||||
assert 'can not set' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
a = 's'
|
||||
|
||||
class cp(c): # pylint: disable=unused-variable
|
||||
# overriding a Property with a Parameter is allowed
|
||||
a = Parameter('x', IntRange())
|
||||
|
||||
|
||||
def test_Properties_mro():
|
||||
class Base(HasProperties):
|
||||
|
148
test/test_statemachine.py
Normal file
148
test/test_statemachine.py
Normal file
@ -0,0 +1,148 @@
|
||||
# -*- 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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from secop.lib.statemachine import StateMachine, Stop, Retry
|
||||
|
||||
|
||||
def rise(state):
|
||||
state.step += 1
|
||||
print('rise', state.step)
|
||||
if state.init:
|
||||
state.status = 'rise'
|
||||
state.level += 1
|
||||
if state.level > 3:
|
||||
return turn
|
||||
return Retry()
|
||||
|
||||
|
||||
def turn(state):
|
||||
state.step += 1
|
||||
if state.init:
|
||||
state.status = 'turn'
|
||||
state.direction += 1
|
||||
if state.direction > 3:
|
||||
return fall
|
||||
return Retry()
|
||||
|
||||
|
||||
def fall(state):
|
||||
state.step += 1
|
||||
if state.init:
|
||||
state.status = 'fall'
|
||||
state.level -= 1
|
||||
if state.level < 0:
|
||||
raise ValueError('crash')
|
||||
return Retry(0) # retry until crash!
|
||||
|
||||
|
||||
def error_handler(state):
|
||||
state.last_error_name = type(state.last_error).__name__
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
class DummyThread:
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
|
||||
def test_fun():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
assert s.step == 0
|
||||
assert s.status == ''
|
||||
s.cycle() # do nothing
|
||||
assert s.step == 0
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.cycle()
|
||||
for i in range(1, 4):
|
||||
assert s.status == 'rise'
|
||||
assert s.step == i
|
||||
assert s.level == i
|
||||
assert s.direction == 0
|
||||
s.cycle()
|
||||
for i in range(5, 8):
|
||||
assert s.status == 'turn'
|
||||
assert s.step == i
|
||||
assert s.level == 4
|
||||
assert s.direction == i - 4
|
||||
s.cycle()
|
||||
s.cycle() # -> crash
|
||||
assert isinstance(s.last_error, ValueError)
|
||||
assert str(s.last_error) == 'crash'
|
||||
assert s.state is None
|
||||
|
||||
|
||||
def test_max_chain():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(fall, level=999+1, direction=0)
|
||||
s.cycle()
|
||||
assert isinstance(s.last_error, RuntimeError)
|
||||
assert s.state is None
|
||||
|
||||
|
||||
def test_stop():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
for _ in range(1, 3):
|
||||
s.cycle()
|
||||
s.stop()
|
||||
s.cycle()
|
||||
assert isinstance(s.last_error, Stop)
|
||||
assert s.state is None
|
||||
|
||||
|
||||
def test_std_error_handling():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.level = None # -> TypeError on next step
|
||||
s.cycle()
|
||||
assert s.state is None # default error handler: stop machine
|
||||
assert isinstance(s.last_error, TypeError)
|
||||
assert not hasattr(s, 'last_error_name')
|
||||
|
||||
|
||||
def test_default_error_handling():
|
||||
s = StateMachine(step=0, status='', cleanup=error_handler, threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.level = None
|
||||
s.cycle()
|
||||
assert s.state is None
|
||||
assert s.last_error_name == 'TypeError'
|
||||
assert isinstance(s.last_error, TypeError)
|
||||
|
||||
|
||||
def test_cleanup_on_restart():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.start(turn)
|
||||
s.cycle()
|
||||
assert s.state is turn
|
||||
assert s.last_error is None
|
Loading…
x
Reference in New Issue
Block a user