/*************************************************************************\ * 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 #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 * #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); // 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