dbUnitTest.h

Helpers for unittests of process database.

Author

Michael Davidsaver, Ralph Lange

Unit testing of record processing

Test skeleton

For the impatient, the skeleton of a test:

#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();
}
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

Discussion:

Some tests require the context of an IOC to be run. This conflicts with the idea of running multiple tests within a test harness, as iocInit() is only allowed to be called once, and some parts of the full IOC (e.g. the rsrv CA server) can not be shut down cleanly. The function iocBuildIsolated() allows to start an IOC without its Channel Access parts, so that it can be shutdown quite cleanly using iocShutdown(). This feature is only intended to be used from test programs, do not use it on production IOCs. After building the IOC using iocBuildIsolated() or iocBuild(), it has to be started by calling iocRun().

The part from iocBuildIsolated() to iocShutdown() can be repeated to execute multiple tests within one executable or harness.

To make it easier to create a single test program that can be built for both the embedded and workstation operating system harnesses, the header file testMain.h provides a convenience macro MAIN() that adjusts the name of the test program according to the platform it is running on: main() on workstations and a regular function name on embedded systems.

Actions

Several helper functions are provided to interact with a running database.

Correct argument types must be used with var-arg functions.

  • 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

testdbPutFieldOk("pvname", DBF_ULONG, (unsigned int)5);
testdbPutFieldOk("pvname", DBF_FLOAT, (double)4.1);
testdbPutFieldOk("pvname", DBF_STRING, "hello world");

See also

enum dbfType in dbFldTypes.h

Monitoring for changes

When Put and Get aren’t sufficient, testMonitor may help to setup and monitor for changes.

Synchronizing

Helpers to synchronize with some database worker threads

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.

epicsEventId evt;
void thread1() {
  evt = epicsEventMustCreate(...);
  // spawn thread2()
  epicsEventMustWait(evt);
  epicsEventDestroy(evt); // <- Racer
}
// ...
void thread2() {
  epicsEventMustSignal(evt); // <- Racer
}

When possible, the best way to avoid this race would be to join the worker before destroying the event.

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);
}

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.

epicsEventId evt;
void thread1() {
  evt = epicsEventMustCreate(...);
  // spawn thread2()
  epicsEventMustWait(evt);
  testGlobalLock();   // <-- added
  epicsEventDestroy(evt);
  testGlobalUnlock(); // <-- added
}
// ...
void thread2() {
  testGlobalLock();   // <-- added
  epicsEventMustSignal(evt);
  testGlobalUnlock(); // <-- added
}

This must be a global mutex to avoid simply shifting the race from the event to a locally allocated mutex.

Typedefs

typedef struct testMonitor testMonitor

Functions

void testdbPrepare(void)

First step in test database setup

See also

Test skeleton

void testdbReadDatabase(const char *file, const char *path, const char *substitutions)

Read .dbd or .db file

See also

Test skeleton

void testIocInitOk(void)

Assert success of iocInit()

See also

Test skeleton

void testIocShutdownOk(void)

Shutdown test database processing.

eg. Stops scan threads

See also

Test skeleton

void testdbCleanup(void)

Final step in test database cleanup

See also

Test skeleton

void testdbPutFieldOk(const char *pv, int dbrType, ...)

Assert that a dbPutField() scalar operation will complete successfully.

testdbPutFieldOk("some.TPRO", DBF_LONG, 1);

See also

Actions

void testdbPutFieldFail(long status, const char *pv, int dbrType, ...)

Assert that a dbPutField() operation will fail with a certain S_* code

See also

Actions

long testdbVPutField(const char *pv, short dbrType, va_list ap)

Assert that a dbPutField() scalar operation will complete successfully.

See also

Actions

void testdbGetFieldEqual(const char *pv, int dbrType, ...)

Assert that a dbGetField() scalar operation will complete successfully, with the provided value.

testdbGetFieldEqual("some.TPRO", DBF_LONG, 0);

See also

Actions

void testdbVGetFieldEqual(const char *pv, short dbrType, va_list ap)

Assert that a dbGetField() scalar operation will complete successfully, with the provided value.

See also

Actions

void testdbPutArrFieldOk(const char *pv, short dbrType, unsigned long count, const void *pbuf)

Assert that a dbPutField() array operation will complete successfully.

static const epicsUInt32 putval[] = {1,2,3};
testdbPutArrFieldOk("some:wf", DBF_ULONG, NELEMENTS(putval), putval);

See also

Actions

Parameters:
  • pv – a PV name, possibly including filter expression

  • dbrType – a DBF_* type code (cf. dbfType in dbFldTypes.h)

  • count – Number of elements in pbuf array

  • pbuf – Array of values to write

void testdbGetArrFieldEqual(const char *pv, short dbfType, long nRequest, unsigned long pbufcnt, const void *pbuf)

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)

Parameters:
  • pv – PV name string

  • dbfType – One of the DBF_* macros from dbAccess.h

  • nRequest – Number of elements to request from pv

  • pbufcnt – Number of elements pointed to be pbuf

  • pbuf – Expected value buffer

dbCommon *testdbRecordPtr(const char *pv)

Obtain pointer to record.

Calls testAbort() on failure. Will never return NULL.

Note

Remember to dbScanLock() when accessing mutable fields.

testMonitor *testMonitorCreate(const char *pvname, unsigned dbe_mask, unsigned opt)

Setup monitoring the named PV for changes

Calls testAbort() on failure. Will never return NULL.

Since

3.16.0.1

Parameters:
  • pvname[in] Requested PV name. Must be valid for dbChannelCreate().

  • dbe_mask[in] A bitwise or of DBE_VALUE and friends.

  • opt[in] Currently unused. Set to zero.

Returns:

Newly allocated testMonitor object, which caller must testMonitorDestroy()

void testMonitorDestroy(testMonitor*)

Stop monitoring

Since

3.16.0.1

void testMonitorWait(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.

Since

3.16.0.1

void testMonitorSync(testMonitor*)

Synchronize with dbEvent working for subscription.

On return, any updates previously posted for this subscriptions have been delivered.

Since

7.0.10

unsigned testMonitorCount(testMonitor*, unsigned reset)

Return the number of monitor events which have occurred 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.

Since

3.16.0.1

void testSyncCallback(void)

Synchronize the shared callback queues.

Block until all callback queue jobs which were queued, or running, have completed.

void testGlobalLock(void)

Lock Global convenience mutex for use by test code.

void testGlobalUnlock(void)

Unlock Global convenience mutex for use by test code.