diff --git a/Makefile b/Makefile index b232814..7500ef2 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,16 @@ include $(TOP)/configure/RULES_TOP UNINSTALL_DIRS += $(wildcard $(INSTALL_LOCATION)/python*) -#useful targets includ: doc-html and doc-clean -doc-%: - PYTHONPATH=$$PWD/python$(PY_VER)/$(EPICS_HOST_ARCH) $(MAKE) -C documentation $* +# jump to a sub-directory where CONFIG_PY has been included +# can't include CONFIG_PY here as it may not exist yet +nose sphinx sh ipython: all + $(MAKE) -C devsupApp/src/O.$(EPICS_HOST_ARCH) $@ PYTHON=$(PYTHON) -doc: doc-html +sphinx-clean: + $(MAKE) -C documentation clean PYTHON=$(PYTHON) + +sphinx-commit: sphinx + touch documentation/_build/html/.nojekyll + ./commit-gh.sh documentation/_build/html + +.PHONY: nose sphinx sphinx-commit sphinx-clean diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE index 11ed558..49c12e8 100644 --- a/configure/CONFIG_SITE +++ b/configure/CONFIG_SITE @@ -39,5 +39,7 @@ PY_VER=2.7 # Python interpreter PYTHON ?= python$(PY_VER) +USR_CPPFLAGS += -DUSE_TYPED_RSET + -include $(TOP)/configure/CONFIG_SITE.local -include $(TOP)/../CONFIG_SITE.local diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 55479e7..6e324b1 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -24,9 +24,12 @@ _dbapi_SRCS += dbapi.c _dbapi_SRCS += dbrec.c _dbapi_SRCS += dbfield.c _dbapi_SRCS += dbdset.c +_dbapi_SRCS += utest.c _dbapi_SRCS += pyDevSupCommon_registerRecordDeviceDriver.cpp +_dbapi_LIBS += $(EPICS_BASE_IOC_LIBS) + PY += devsup/__init__.py PY += devsup/_nullapi.py PY += devsup/db.py @@ -37,6 +40,9 @@ PY += devsup/util.py PY += devsup/disect.py PY += devsup/ptable.py +PY += devsup/test/__init__.py +PY += devsup/test/test_db.py + #=========================== include $(TOP)/configure/RULES @@ -52,3 +58,20 @@ pyconfig: @echo "Library path: $(PY_LIBDIRS)" @echo "USR_CPPFLAGS: $(USR_CPPFLAGS)" @echo "USR_LDFLAGS: $(USR_LDFLAGS)" + +ifneq (,$(T_A)) +nose: + PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(PYTHON) -m nose -P devsup $(NOSEFLAGS) + +# bounce back down to the sphinx generated Makefile +# aren't Makefiles fun... +sphinx: + PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(MAKE) -C $(TOP)/documentation html + +sh: + echo "export PYTHONPATH=\$${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" > $(OUTPUT) + +ipython: + PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(PYTHON) -c "import sys; sys.argv[0] = '$(PYTHON)'; from IPython.terminal.ipapp import launch_new_instance; launch_new_instance()" + +endif diff --git a/devsupApp/src/dbapi.c b/devsupApp/src/dbapi.c index 6632901..200e4f4 100644 --- a/devsupApp/src/dbapi.c +++ b/devsupApp/src/dbapi.c @@ -194,9 +194,9 @@ PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws) } Py_BEGIN_ALLOW_THREADS { - if(fname) + if(fname) { status = dbReadDatabase(&pdbbase, fname, path, sub); - else { + } else { FILE *ff = fdopen(fd, "r"); status = dbReadDatabaseFP(&pdbbase, ff, path, sub); // dbReadDatabaseFP() has called fclose() @@ -213,18 +213,31 @@ PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws) } static -PyObject *py_iocInit(PyObject *unused) +PyObject *py_iocInit(PyObject *unused, PyObject *args, PyObject *kws) { + static char* names[] = {"isolate", NULL}; + PyObject *pyisolate = Py_True; + int isolate, ret; + if(!PyArg_ParseTupleAndKeywords(args, kws, "|O", names, &pyisolate)) + return NULL; + + isolate = PyObject_IsTrue(pyisolate); Py_BEGIN_ALLOW_THREADS { - iocInit(); + ret = isolate ? iocBuildIsolated() : iocBuild(); + if(!ret) + ret = iocRun(); } Py_END_ALLOW_THREADS + if(ret) + return PyErr_Format(PyExc_RuntimeError, "Error %d", ret); + Py_RETURN_NONE; } static PyObject *py_pyDevSupCommon(PyObject *unused) { + static int once; Py_BEGIN_ALLOW_THREADS { pyDevSupCommon_registerRecordDeviceDriver(pdbbase); } Py_END_ALLOW_THREADS @@ -258,7 +271,7 @@ static struct PyModuleDef dbapimodule = { PyMODINIT_FUNC init_dbapi(void) { - PyObject *mod = NULL, *hookdict, *vertup, *obj; + PyObject *mod = NULL, *hookdict, *vertup; pystate *st; pyDevReasonID = epicsThreadPrivateCreate(); @@ -354,19 +367,12 @@ PyMODINIT_FUNC init_dbapi(void) if(vertup) PyModule_AddObject(mod, "pydevver", vertup); -#if PY_MAJOR_VERSION >= 3 || (PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION>=7) - obj = PyCapsule_New(pdbbase, "pdbbase", NULL); -#else - obj = PyCObject_FromVoidPtrAndDesc(pdbbase, "pdbbase", NULL); -#endif - if(!obj) - goto fail; - PyModule_AddObject(mod, "pdbbase", obj); - if(pyField_prepare(mod)) goto fail; if(pyRecord_prepare(mod)) goto fail; + if(pyUTest_prepare(mod)) + goto fail; MODINIT_RET(mod); diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index f7a6988..bf051fc 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -22,16 +22,11 @@ from ._dbapi import (EPICS_VERSION_STRING, __all__ = [] -_ready = [False] - def _init(iocMain=False): - if _ready[0]: - return - _ready[0] = True - if not iocMain: # we haven't read/register base.dbd - _dbapi.dbReadDatabase(os.path.join(XEPICS_BASE, "dbd", "base.dbd")) + _dbapi.dbReadDatabase(os.path.join(XEPICS_BASE, "dbd", "base.dbd"), + path=os.path.join(XEPICS_BASE, "dbd")) _dbapi._dbd_rrd_base() with tempfile.NamedTemporaryFile() as F: diff --git a/devsupApp/src/devsup/test/__init__.py b/devsupApp/src/devsup/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py new file mode 100644 index 0000000..310d011 --- /dev/null +++ b/devsupApp/src/devsup/test/test_db.py @@ -0,0 +1,82 @@ + +import os +import unittest +import tempfile + +from ..db import getRecord +from .. import _dbapi +from .. import _init + +# short-circuit warning from _dbapi._init() +os.environ['TOP'] = _dbapi.XPYDEV_BASE + +class IOCHelper(unittest.TestCase): + db = None + autostart = running = False + def setUp(self): + print("testdbPrepare()") + _dbapi._UTest.testdbPrepare() + _init(iocMain=False) # load base.dbd + + if self.db is not None: + with tempfile.NamedTemporaryFile() as F: + F.write(self.db) + F.flush() + _dbapi.dbReadDatabase(F.name) + + if self.autostart: + self.iocInit() + + def tearDown(self): + self.iocShutdown(); + print("testdbCleanup()") + _dbapi._UTest.testdbCleanup() + + def iocInit(self): + if not self.running: + print("testIocInitOk") + _dbapi._UTest.testIocInitOk() + self.running = True + + def iocShutdown(self): + if self.running: + print("testIocShutdownOk") + _dbapi._UTest.testIocShutdownOk() + self.running = False + +class TestIOC(IOCHelper): + def test_base(self): + pass + + def test_start(self): + self.iocInit() + self.iocShutdown() + + def test_db(self): + with tempfile.NamedTemporaryFile() as F: + F.write('record(longin, "test") {}\n') + F.flush() + _dbapi.dbReadDatabase(F.name) + + rec = getRecord("test") + self.assertEqual(rec.VAL, 0) + rec.VAL = 5 + self.assertEqual(rec.VAL, 5) + +class TestScan(IOCHelper): + db = """ + record(longout, src) { + field(OUT, "tgt PP") + } + record(longin, "tgt") {} + """ + autostart = True + + def test_link(self): + src, tgt = getRecord('src'), getRecord('tgt') + + src.VAL = 42 + self.assertEqual(src.VAL, 42) + self.assertEqual(tgt.VAL, 0) + src.scan(sync=True) + self.assertEqual(tgt.VAL, 42) diff --git a/devsupApp/src/pydevsup.h b/devsupApp/src/pydevsup.h index 76bcf28..93e75d8 100644 --- a/devsupApp/src/pydevsup.h +++ b/devsupApp/src/pydevsup.h @@ -20,6 +20,8 @@ initHookState pyInitLastState; PyObject* pyDBD_setup(PyObject *unused); PyObject* pyDBD_cleanup(PyObject *unused); +int pyUTest_prepare(PyObject *module); + int pyField_prepare(PyObject *module); int pyRecord_prepare(PyObject *module); diff --git a/devsupApp/src/utest.c b/devsupApp/src/utest.c new file mode 100644 index 0000000..344c5bb --- /dev/null +++ b/devsupApp/src/utest.c @@ -0,0 +1,121 @@ + +/* python has its own ideas about which version to support */ +#undef _POSIX_C_SOURCE +#undef _XOPEN_SOURCE + +#include + +#include +#include +#include +#include + +#include "pydevsup.h" + +static dbEventCtx testEvtCtx; + +typedef struct { + PyObject_HEAD +} UTest; + +static PyTypeObject UTest_type = { +#if PY_MAJOR_VERSION >= 3 + PyVarObject_HEAD_INIT(NULL, 0) +#else + PyObject_HEAD_INIT(NULL) + 0, +#endif + "_dbapi._UTest", + sizeof(UTest), +}; + +static PyObject* utest_prepare(PyObject *unused) +{ + Py_BEGIN_ALLOW_THREADS { + //testdbPrepare(); doesn't do anything essential for us as of 7.0.2 + } Py_END_ALLOW_THREADS + Py_RETURN_NONE; +} + +static PyObject* utest_init(PyObject *unused) +{ + int ret; + if(testEvtCtx) + return PyErr_Format(PyExc_RuntimeError, "Missing testIocShutdownOk()"); + + // like, testIocInitOk() without testAbort() + Py_BEGIN_ALLOW_THREADS { + eltc(0); + ret = iocBuildIsolated() || iocRun(); + eltc(1); + } Py_END_ALLOW_THREADS + if(ret) { + return PyErr_Format(PyExc_RuntimeError, "iocInit fails with %d", ret); + } + + Py_BEGIN_ALLOW_THREADS { + testEvtCtx=db_init_events(); + } Py_END_ALLOW_THREADS + if(!testEvtCtx) { + iocShutdown(); + return PyErr_Format(PyExc_RuntimeError, "iocInit fails create dbEvent context"); + } + + Py_BEGIN_ALLOW_THREADS { + ret = db_start_events(testEvtCtx, "CAS-test-py", NULL, NULL, epicsThreadPriorityCAServerLow); + } Py_END_ALLOW_THREADS + if(ret!=DB_EVENT_OK) { + db_close_events(testEvtCtx); + testEvtCtx = NULL; + iocShutdown(); + return PyErr_Format(PyExc_RuntimeError, "db_start_events fails with %d", ret); + } + Py_RETURN_NONE; +} + +static PyObject* utest_shutdown(PyObject *unused) +{ + Py_BEGIN_ALLOW_THREADS { + //testIocShutdownOk(); + db_close_events(testEvtCtx); + testEvtCtx = NULL; + iocShutdown(); + } Py_END_ALLOW_THREADS + Py_RETURN_NONE; +} + +static PyObject* utest_cleanup(PyObject *unused) +{ + Py_BEGIN_ALLOW_THREADS { + testdbCleanup(); + errlogFlush(); + } Py_END_ALLOW_THREADS + Py_RETURN_NONE; +} + +static PyMethodDef UTest_methods[] = { + {"testdbPrepare", (PyCFunction)&utest_prepare, METH_STATIC|METH_NOARGS, ""}, + {"testIocInitOk", (PyCFunction)&utest_init, METH_STATIC|METH_NOARGS, ""}, + {"testIocShutdownOk", (PyCFunction)&utest_shutdown, METH_STATIC|METH_NOARGS, ""}, + {"testdbCleanup", (PyCFunction)&utest_cleanup, METH_STATIC|METH_NOARGS, ""}, + {NULL} +}; + +int pyUTest_prepare(PyObject *module) +{ + PyObject *typeobj=(PyObject*)&UTest_type; + + UTest_type.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE; + UTest_type.tp_methods = UTest_methods; + + if(PyType_Ready(&UTest_type)<0) + return -1; + + Py_INCREF(typeobj); + if(PyModule_AddObject(module, "_UTest", typeobj)) { + Py_DECREF(typeobj); + return -1; + } + + return 0; +}