From 6ed6dc11bb26d6883f67c2b3c9d299eb49704b4d Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Thu, 1 Apr 2021 10:18:49 -0700 Subject: [PATCH] doc dbUnitTest.h --- modules/database/src/ioc/db/dbUnitTest.h | 310 ++++++++++++++++++----- 1 file changed, 243 insertions(+), 67 deletions(-) diff --git a/modules/database/src/ioc/db/dbUnitTest.h b/modules/database/src/ioc/db/dbUnitTest.h index db0c269c2..db8218a86 100644 --- a/modules/database/src/ioc/db/dbUnitTest.h +++ b/modules/database/src/ioc/db/dbUnitTest.h @@ -6,9 +6,11 @@ * in file LICENSE that is included with this distribution. \*************************************************************************/ -/* - * Author: Michael Davidsaver - * Ralph Lange +/** @file dbUnitTest.h + * @brief Helpers for unitests of process database + * @author Michael Davidsaver, Ralph Lange + * + * @see @ref dbunittest */ #ifndef EPICSUNITTESTDB_H @@ -26,38 +28,88 @@ extern "C" { #endif +/** First step in test database setup + * + * @see @ref dbtestskel + */ epicsShareFunc void testdbPrepare(void); +/** Read .dbd or .db file + * + * @see @ref dbtestskel + */ epicsShareFunc void testdbReadDatabase(const char* file, const char* path, const char* substitutions); +/** Assert success of iocInit() + * + * @see @ref dbtestskel + */ epicsShareFunc void testIocInitOk(void); +/** Shutdown test database processing. + * + * eg. Stops scan threads + * + * @see @ref dbtestskel + */ epicsShareFunc void testIocShutdownOk(void); +/** Final step in test database cleanup + * + * @see @ref dbtestskel + */ epicsShareFunc void testdbCleanup(void); -/* Correct argument types must be used with this var-arg function! - * Doing otherwise will result in corruption of argument values! +/** Assert that a dbPutField() scalar operation will complete successfully. * - * int for DBR_UCHAR, DBR_CHAR, DBR_USHORT, DBR_SHORT, DBR_LONG - * unsigned int for DBR_ULONG - * long long for DBF_INT64 - * unsigned long long for DBF_UINT64 - * double for DBR_FLOAT and DBR_DOUBLE - * const char* for DBR_STRING + * @code + * testdbPutFieldOk("some.TPRO", DBF_LONG, 1); + * @endcode * - * eg. - * testdbPutFieldOk("pvname", DBF_ULONG, (unsigned int)5); - * testdbPutFieldOk("pvname", DBF_FLOAT, (double)4.1); - * testdbPutFieldOk("pvname", DBF_STRING, "hello world"); + * @see @ref dbtestactions */ epicsShareFunc void testdbPutFieldOk(const char* pv, int dbrType, ...); -/* Tests for put failure */ + +/** Assert that a dbPutField() operation will fail with a certain S_\* code + * + * @see @ref dbtestactions + */ epicsShareFunc void testdbPutFieldFail(long status, const char* pv, int dbrType, ...); +/** Assert that a dbPutField() scalar operation will complete successfully. + * + * @see @ref dbtestactions + */ epicsShareFunc long testdbVPutField(const char* pv, short dbrType, va_list ap); +/** Assert that a dbGetField() scalar operation will complete successfully, with the provided value. + * + * @code + * testdbGetFieldEqual("some.TPRO", DBF_LONG, 0); + * @endcode + * + * @see @ref dbtestactions + */ epicsShareFunc void testdbGetFieldEqual(const char* pv, int dbrType, ...); + +/** Assert that a dbGetField() scalar operation will complete successfully, with the provided value. + * + * @see @ref dbtestactions + */ epicsShareFunc void testdbVGetFieldEqual(const char* pv, short dbrType, va_list ap); +/** Assert that a dbPutField() array operation will complete successfully. + * + * @param pv a PV name, possibly including filter expression + * @param a DBF_\* type code (cf. dbfType in dbFldTypes.h) + * @param count Number of elements in pbuf array + * @param pbuf Array of values to write + * + * @code + * static const epicsUInt32 putval[] = {1,2,3}; + * testdbVGetFieldEqual("some:wf", DBF_ULONG, NELEMENTS(putval), putval); + * @endcode + * + * @see @ref dbtestactions + */ epicsShareFunc void testdbPutArrFieldOk(const char* pv, short dbrType, unsigned long count, const void *pbuf); /** @@ -77,20 +129,26 @@ epicsShareFunc void testdbPutArrFieldOk(const char* pv, short dbrType, unsigned */ epicsShareFunc void testdbGetArrFieldEqual(const char* pv, short dbfType, long nRequest, unsigned long pbufcnt, const void *pbuf); +/** Obtain pointer to record. + * + * Calls testAbort() on failure. Will never return NULL. + * + * @note Remember to dbScanLock() when accessing mutable fields. + */ epicsShareFunc dbCommon* testdbRecordPtr(const char* pv); typedef struct testMonitor testMonitor; -/* Begin monitoring the named PV for changes */ +/** Setup monitoring the named PV for changes */ epicsShareFunc testMonitor* testMonitorCreate(const char* pvname, unsigned dbe_mask, unsigned opt); -/* End monitoring */ +/** Stop monitoring */ epicsShareFunc void testMonitorDestroy(testMonitor*); -/* Return immediately if it has been updated since create, last wait, +/** Return immediately if it has been updated since create, last wait, * or reset (count w/ reset=1). * Otherwise, block until the value of the target PV is updated. */ epicsShareFunc void testMonitorWait(testMonitor*); -/* Return the number of monitor events which have occured since create, +/** Return the number of monitor events which have occured since create, * or a pervious reset (called reset=1). * Calling w/ reset=0 only returns the count. * Calling w/ reset=1 resets the count to zero and ensures that the next @@ -106,60 +164,178 @@ epicsShareFunc unsigned testMonitorCount(testMonitor*, unsigned reset); */ epicsShareFunc void testSyncCallback(void); -/** Global mutex for use by test code. +/** Lock Global convenience mutex for use by test code. * - * This utility mutex is intended to be used to avoid races in situations - * where some other syncronization primitive is being destroyed (epicsEvent, - * epicsMutex, ...). - * - * For example. The following has a subtle race where the event may be - * destroyed (free()'d) before the call to epicsEventMustSignal() has - * returned. On some targets this leads to a use after free() error. - * - @code - epicsEventId evt; - void thread1() { - evt = epicsEventMustCreate(...); - // spawn thread2() - epicsEventMustWait(evt); - epicsEventDestroy(evt); - } - // ... - void thread2() { - epicsEventMustSignal(evt); - } - @endcode - * - * One way to avoid this race is to use a global mutex to ensure - * that epicsEventMustSignal() has returned before destroying - * the event. - * - @code - epicsEventId evt; - void thread1() { - evt = epicsEventMustCreate(...); - // spawn thread2() - epicsEventMustWait(evt); - testGlobalLock(); // <-- added - epicsEventDestroy(evt); - testGlobalUnlock(); // <-- added - } - // ... - void thread2() { - testGlobalLock(); // <-- added - epicsEventMustSignal(evt); - testGlobalUnlock(); // <-- added - } - @endcode - * - * This must be a global mutex to avoid simply shifting the race - * from the event to a locally allocated mutex. + * @see @ref dbtestmutex */ epicsShareFunc void testGlobalLock(void); + +/** Unlock Global convenience mutex for use by test code. + * + * @see @ref dbtestmutex + */ epicsShareFunc void testGlobalUnlock(void); #ifdef __cplusplus } #endif +/** @page dbunittest Unit testing of record processing + * + * @see @ref epicsUnitTest.h + * + * @section dbtestskel Test skeleton + * + * For the impatient, the skeleton of a test: + * + * @code + * #include + * + * int mytest_registerRecordDeviceDriver(DBBASE *pbase); + * void testCase(void) { + * testdbPrepare(); + * testdbReadDatabase("mytest.dbd", 0, 0); + * mytest_registerRecordDeviceDriver(pdbbase); + * testdbReadDatabase("some.db", 0, "VAR=value"); + * testIocInitOk(); + * // database running ... + * testIocShutdownOk(); + * testdbCleanup(); + * } + * + * MAIN(mytestmain) { + * testPlan(0); + * testCase(); + * testCase(); // may be repeated if desirable. + * return testDone(); + * } + * @endcode + * + * @code + * TARGETS += $(COMMON_DIR)/mytest.dbd + * DBDDEPENDS_FILES += mytest.dbd$(DEP) + * TESTFILES += $(COMMON_DIR)/mytest.dbd + * mytest_DBD += base.dbd + * mytest_DBD += someother.dbd + * + * TESTPROD_HOST += mytest + * mytest_SRCS += mytestmain.c # see above + * mytest_SRCS += mytest_registerRecordDeviceDriver.cpp + * TESTFILES += some.db + * @endcode + * + * @section dbtestactions Actions + * + * Several helper functions are provided to interact with a running database. + * + * @li testdbPutFieldOk() + * @li testdbPutFieldFail() + * @li testdbPutArrFieldOk() + * @li testdbVPutField() + * @li testdbGetFieldEqual() + * @li testdbVGetFieldEqual() + * + * Correct argument types must be used with var-arg functions. + * + * @li int for DBR_UCHAR, DBR_CHAR, DBR_USHORT, DBR_SHORT, DBR_LONG + * @li unsigned int for DBR_ULONG + * @li long long for DBF_INT64 + * @li unsigned long long for DBF_UINT64 + * @li double for DBR_FLOAT and DBR_DOUBLE + * @li const char* for DBR_STRING + * + * @see enum dbfType in dbFldTypes.h + * + * @code + * testdbPutFieldOk("pvname", DBF_ULONG, (unsigned int)5); + * testdbPutFieldOk("pvname", DBF_FLOAT, (double)4.1); + * testdbPutFieldOk("pvname", DBF_STRING, "hello world"); + * @endcode + * + * @section dbtestmonitor Monitoring for changes + * + * When Put and Get aren't sufficient, testMonitor may help to setup and monitor for changes. + * + * @li testMonitorCreate() + * @li testMonitorDestroy() + * @li testMonitorWait() + * @li testMonitorCount() + * + * @section dbtestsync Synchronizing + * + * Helpers to synchronize with some database worker threads + * + * @li testSyncCallback() + * + * @section dbtestmutex Global mutex for use by test code. + * + * This utility mutex is intended to be used to avoid races in situations + * where some other synchronization primitive is being destroyed (epicsEvent, + * epicsMutex, ...) and use of epicsThreadMustJoin() is impractical. + * + * For example. The following has a subtle race where the event may be + * destroyed (free()'d) before the call to epicsEventMustSignal() has + * returned. On some targets this leads to a use after free() error. + * + * @code + * epicsEventId evt; + * void thread1() { + * evt = epicsEventMustCreate(...); + * // spawn thread2() + * epicsEventMustWait(evt); + * epicsEventDestroy(evt); // <- Racer + * } + * // ... + * void thread2() { + * epicsEventMustSignal(evt); // <- Racer + * } + * @endcode + * + * When possible, the best way to avoid this race would be to join the worker + * before destroying the event. + * + * @code + * epicsEventId evt; + * void thread1() { + * epicsThreadOpts opts = EPICS_THREAD_OPTS_INIT; + * epicsThreadId t2; + * opts.joinable = 1; + * evt = epicsEventMustCreate(...); + * t2 = epicsThreadCreateOpt("thread2", &thread2, NULL, &opts); + * assert(t2); + * epicsEventMustWait(evt); + * epicsThreadMustJoin(t2); + * epicsEventDestroy(evt); + * } + * void thread2() { + * epicsEventMustSignal(evt); + * } + * @endcode + * + * Another way to avoid this race is to use a global mutex to ensure + * that epicsEventMustSignal() has returned before destroying the event. + * testGlobalLock() and testGlobalUnlock() provide access to such a mutex. + * + * @code + * epicsEventId evt; + * void thread1() { + * evt = epicsEventMustCreate(...); + * // spawn thread2() + * epicsEventMustWait(evt); + * testGlobalLock(); // <-- added + * epicsEventDestroy(evt); + * testGlobalUnlock(); // <-- added + * } + * // ... + * void thread2() { + * testGlobalLock(); // <-- added + * epicsEventMustSignal(evt); + * testGlobalUnlock(); // <-- added + * } + * @endcode + * + * This must be a global mutex to avoid simply shifting the race + * from the event to a locally allocated mutex. + */ + #endif // EPICSUNITTESTDB_H