Files
epics-base/modules/database/src/ioc/db/dbUnitTest.h
Michael Davidsaver 6be0372257 doc
2022-12-21 09:50:07 -08:00

348 lines
9.8 KiB
C

/*************************************************************************\
* Copyright (c) 2013 Brookhaven National Laboratory.
* Copyright (c) 2013 ITER Organization.
* SPDX-License-Identifier: EPICS
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/
/** @file dbUnitTest.h
* @brief Helpers for unittests of process database
* @author Michael Davidsaver, Ralph Lange
*
* @see @ref dbunittest
*/
#ifndef EPICSUNITTESTDB_H
#define EPICSUNITTESTDB_H
#include <stdarg.h>
#include "epicsUnitTest.h"
#include "dbAddr.h"
#include "dbCommon.h"
#include "dbCoreAPI.h"
#ifdef __cplusplus
extern "C" {
#endif
/** First step in test database setup
*
* @see @ref dbtestskel
*/
DBCORE_API void testdbPrepare(void);
/** Read .dbd or .db file
*
* @see @ref dbtestskel
*/
DBCORE_API void testdbReadDatabase(const char* file,
const char* path,
const char* substitutions);
/** Assert success of iocInit()
*
* @see @ref dbtestskel
*/
DBCORE_API void testIocInitOk(void);
/** Shutdown test database processing.
*
* eg. Stops scan threads
*
* @see @ref dbtestskel
*/
DBCORE_API void testIocShutdownOk(void);
/** Final step in test database cleanup
*
* @see @ref dbtestskel
*/
DBCORE_API void testdbCleanup(void);
/** Assert that a dbPutField() scalar operation will complete successfully.
*
* @code
* testdbPutFieldOk("some.TPRO", DBF_LONG, 1);
* @endcode
*
* @see @ref dbtestactions
*/
DBCORE_API void testdbPutFieldOk(const char* pv, int dbrType, ...);
/** Assert that a dbPutField() operation will fail with a certain S_\* code
*
* @see @ref dbtestactions
*/
DBCORE_API void testdbPutFieldFail(long status, const char* pv, int dbrType, ...);
/** Assert that a dbPutField() scalar operation will complete successfully.
*
* @see @ref dbtestactions
*/
DBCORE_API 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
*/
DBCORE_API void testdbGetFieldEqual(const char* pv, int dbrType, ...);
/** Assert that a dbGetField() scalar operation will complete successfully, with the provided value.
*
* @see @ref dbtestactions
*/
DBCORE_API 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 dbrType 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};
* testdbPutArrFieldOk("some:wf", DBF_ULONG, NELEMENTS(putval), putval);
* @endcode
*
* @see @ref dbtestactions
*/
DBCORE_API void testdbPutArrFieldOk(const char* pv, short dbrType, unsigned long count, const void *pbuf);
/**
* @param pv PV name string
* @param dbfType One of the DBF_\* macros from dbAccess.h
* @param nRequest Number of elements to request from pv
* @param pbufcnt Number of elements pointed to be pbuf
* @param pbuf Expected value buffer
*
* Execute dbGet() of nRequest elements and compare the result with
* pbuf (pbufcnt is an element count).
* Element size is derived from dbfType.
*
* nRequest > pbufcnt will detect truncation.
* nRequest < pbufcnt always fails.
* nRequest ==pbufcnt checks prefix (actual may be longer than expected)
*/
DBCORE_API 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.
*/
DBCORE_API dbCommon* testdbRecordPtr(const char* pv);
typedef struct testMonitor testMonitor;
/** Setup monitoring the named PV for changes */
DBCORE_API testMonitor* testMonitorCreate(const char* pvname, unsigned dbe_mask, unsigned opt);
/** Stop monitoring */
DBCORE_API void testMonitorDestroy(testMonitor*);
/** 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.
*/
DBCORE_API void testMonitorWait(testMonitor*);
/** Return the number of monitor events which have occured since create,
* or a previous 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
* wait will block unless subsequent events occur. Returns the previous
* count.
*/
DBCORE_API unsigned testMonitorCount(testMonitor*, unsigned reset);
/** Synchronize the shared callback queues.
*
* Block until all callback queue jobs which were queued, or running,
* have completed.
*/
DBCORE_API void testSyncCallback(void);
/** Lock Global convenience mutex for use by test code.
*
* @see @ref dbtestmutex
*/
DBCORE_API void testGlobalLock(void);
/** Unlock Global convenience mutex for use by test code.
*
* @see @ref dbtestmutex
*/
DBCORE_API 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 <dbUnitTest.h>
* #include <testMain.h>
*
* 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); // adjust number of tests
* testCase();
* testCase(); // may be repeated if desirable.
* return testDone();
* }
* @endcode
*
* @code
* TOP = ..
* include $(TOP)/configure/CONFIG
*
* 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
*
* include $(TOP)/configure/RULES
* @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