Unit testing of record processing

page dbunittest

See also

epicsUnitTest.h

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

Actions

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

  • testdbPutFieldOk()

  • testdbPutFieldFail()

  • testdbPutArrFieldOk()

  • testdbVPutField()

  • testdbGetFieldEqual()

  • testdbVGetFieldEqual()

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

See also

enum dbfType in dbFldTypes.h

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

Monitoring for changes

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

  • testMonitorCreate()

  • testMonitorDestroy()

  • testMonitorWait()

  • testMonitorCount()

Synchronizing

Helpers to synchronize with some database worker threads

  • testSyncCallback()

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.