diff --git a/src/ioc/db/callback.c b/src/ioc/db/callback.c index 719f5ced6..a3ae657df 100644 --- a/src/ioc/db/callback.c +++ b/src/ioc/db/callback.c @@ -146,7 +146,12 @@ static void callbackTask(void *arg) while(TRUE) { void *ptr; epicsEventMustWait(callbackSem[priority]); - while((ptr = epicsRingPointerPop(callbackQ[priority]))) { + while ((ptr = epicsRingPointerPop(callbackQ[priority]))) { + /* Retrigger if there are more threads and more work */ + if (callbackThreadsRunning[priority] > 1 + && !epicsRingPointerIsEmpty(callbackQ[priority])) { + epicsEventTrigger(callbackSem[priority]); + } CALLBACK *pcallback = (CALLBACK *)ptr; if (ptr == &exitCallback) goto shutdown; ringOverflow[priority] = FALSE; @@ -220,21 +225,21 @@ void callbackInit(void) } /* This routine can be called from interrupt context */ -void callbackRequest(CALLBACK *pcallback) +int callbackRequest(CALLBACK *pcallback) { int priority; int pushOK; if (!pcallback) { epicsInterruptContextMessage("callbackRequest: pcallback was NULL\n"); - return; + return S_db_notInit; } priority = pcallback->priority; if (priority < 0 || priority >= NUM_CALLBACK_PRIORITIES) { epicsInterruptContextMessage("callbackRequest: Bad priority\n"); - return; + return S_db_badChoice; } - if (ringOverflow[priority]) return; + if (ringOverflow[priority]) return S_db_bufFull; pushOK = epicsRingPointerPush(callbackQ[priority], pcallback); @@ -245,8 +250,10 @@ void callbackRequest(CALLBACK *pcallback) strcat(msg, " ring buffer full\n"); epicsInterruptContextMessage(msg); ringOverflow[priority] = TRUE; + return S_db_bufFull; } epicsEventSignal(callbackSem[priority]); + return 0; } static void ProcessCallback(CALLBACK *pcallback) @@ -267,11 +274,11 @@ void callbackSetProcess(CALLBACK *pcallback, int Priority, void *pRec) callbackSetUser(pRec, pcallback); } -void callbackRequestProcessCallback(CALLBACK *pcallback, +int callbackRequestProcessCallback(CALLBACK *pcallback, int Priority, void *pRec) { callbackSetProcess(pcallback, Priority, pRec); - callbackRequest(pcallback); + return callbackRequest(pcallback); } static void notify(void *pPrivate) diff --git a/src/ioc/db/callback.h b/src/ioc/db/callback.h index ae06406b5..2732f9d5e 100644 --- a/src/ioc/db/callback.h +++ b/src/ioc/db/callback.h @@ -57,11 +57,11 @@ typedef void (*CALLBACKFUNC)(struct callbackPvt*); ( (USER) = (void *)((CALLBACK *)(PCALLBACK))->user ) epicsShareFunc void callbackInit(void); -epicsShareFunc void callbackRequest(CALLBACK *pCallback); epicsShareFunc void callbackShutdown(void); +epicsShareFunc int callbackRequest(CALLBACK *pCallback); epicsShareFunc void callbackSetProcess( CALLBACK *pcallback, int Priority, void *pRec); -epicsShareFunc void callbackRequestProcessCallback( +epicsShareFunc int callbackRequestProcessCallback( CALLBACK *pCallback,int Priority, void *pRec); epicsShareFunc void callbackRequestDelayed( CALLBACK *pCallback,double seconds); diff --git a/src/ioc/db/dbAccessDefs.h b/src/ioc/db/dbAccessDefs.h index ba794a520..31ffc18c2 100644 --- a/src/ioc/db/dbAccessDefs.h +++ b/src/ioc/db/dbAccessDefs.h @@ -199,6 +199,8 @@ struct dbr_alDouble {DBRalDouble}; #define S_db_cntSpwn (M_dbAccess|63) /*Cannot spawn dbContTask*/ #define S_db_cntCont (M_dbAccess|65) /*Cannot resume dbContTask*/ #define S_db_noMemory (M_dbAccess|66) /*unable to allocate data structure from pool*/ +#define S_db_notInit (M_dbAccess|67) /*Not initialized*/ +#define S_db_bufFull (M_dbAccess|68) /*Buffer full*/ epicsShareFunc long dbPutSpecial(struct dbAddr *paddr,int pass); epicsShareFunc struct rset * dbGetRset(const struct dbAddr *paddr); diff --git a/src/ioc/db/dbScan.c b/src/ioc/db/dbScan.c index a1ba12d9a..7f2337178 100644 --- a/src/ioc/db/dbScan.c +++ b/src/ioc/db/dbScan.c @@ -122,6 +122,8 @@ typedef struct io_scan_list { CALLBACK callback; scan_list scan_list; struct io_scan_list *next; + io_scan_complete cb; + void * arg; } io_scan_list; static io_scan_list *iosl_head[NUM_CALLBACK_PRIORITIES] = { @@ -507,19 +509,31 @@ void scanIoInit(IOSCANPVT *ppioscanpvt) } } - -void scanIoRequest(IOSCANPVT pioscanpvt) +/* return a bit mask indicating each prioity level + * in which a callback request was queued. + */ +unsigned int scanIoRequest(IOSCANPVT pioscanpvt) { int prio; + unsigned int queued = 0; - if (scanCtl != ctlRun) return; + if (scanCtl != ctlRun) return 0; for (prio = 0; prio < NUM_CALLBACK_PRIORITIES; prio++) { io_scan_list *piosl = &pioscanpvt[prio]; if (ellCount(&piosl->scan_list.list) > 0) - callbackRequest(&piosl->callback); + if(!callbackRequest(&piosl->callback)) + queued |= 1<cb = cb; + pioscanpvt->arg = arg; +} + void scanOnce(struct dbCommon *precord) { static int newOverflow = TRUE; @@ -747,9 +761,15 @@ static void spawnPeriodic(int ind) static void ioeventCallback(CALLBACK *pcallback) { io_scan_list *piosl; + io_scan_list *pioslLow; callbackGetUser(piosl, pcallback); scanList(&piosl->scan_list); + pioslLow = piosl - pcallback->priority; + if(pioslLow->cb) + (*pioslLow->cb)(pioslLow->arg, + pioslLow, + pcallback->priority); } static void printList(scan_list *psl, char *message) diff --git a/src/ioc/db/dbScan.h b/src/ioc/db/dbScan.h index 03117f769..42ba46c37 100644 --- a/src/ioc/db/dbScan.h +++ b/src/ioc/db/dbScan.h @@ -39,6 +39,8 @@ struct io_scan_list; typedef struct io_scan_list *IOSCANPVT; typedef struct event_list *EVENTPVT; +typedef void (*io_scan_complete)(void *, IOSCANPVT, int); + struct dbCommon; epicsShareFunc long scanInit(void); @@ -65,7 +67,8 @@ epicsShareFunc int scanpel(const char *event_name); epicsShareFunc int scanpiol(void); epicsShareFunc void scanIoInit(IOSCANPVT *); -epicsShareFunc void scanIoRequest(IOSCANPVT); +epicsShareFunc unsigned int scanIoRequest(IOSCANPVT); +epicsShareFunc void scanIoSetComplete(IOSCANPVT, io_scan_complete, void*); #ifdef __cplusplus } diff --git a/src/ioc/db/test/Makefile b/src/ioc/db/test/Makefile index dd98ffa9e..fa186a601 100644 --- a/src/ioc/db/test/Makefile +++ b/src/ioc/db/test/Makefile @@ -65,6 +65,18 @@ dbStateTest_SRCS += dbStateTest.c testHarness_SRCS += dbStateTest.c TESTS += dbStateTest +TARGETS += $(COMMON_DIR)/scanIoTest.dbd +scanIoTest_DBD += menuGlobal.dbd +scanIoTest_DBD += menuConvert.dbd +scanIoTest_DBD += yRecord.dbd +TESTPROD_HOST += scanIoTest +scanIoTest_SRCS += scanIoTest.c +scanIoTest_SRCS += scanIoTest_registerRecordDeviceDriver.cpp +testHarness_SRCS += scanIoTest.c +testHarness_SRCS += scanIoTest_registerRecordDeviceDriver.cpp +TESTFILES += $(COMMON_DIR)/scanIoTest.dbd ../scanIoTest.db +TESTS += scanIoTest + TESTPROD_HOST += dbChannelTest dbChannelTest_SRCS += dbChannelTest.c dbChannelTest_SRCS += dbTestIoc_registerRecordDeviceDriver.cpp @@ -109,3 +121,4 @@ TESTSCRIPTS_HOST += $(TESTS:%=%.t) include $(TOP)/configure/RULES xRecord$(DEP): $(COMMON_DIR)/xRecord.h +scanIoTest$(DEP): $(COMMON_DIR)/yRecord.h diff --git a/src/ioc/db/test/epicsRunDbTests.c b/src/ioc/db/test/epicsRunDbTests.c index ae150b55a..0217436db 100644 --- a/src/ioc/db/test/epicsRunDbTests.c +++ b/src/ioc/db/test/epicsRunDbTests.c @@ -35,6 +35,7 @@ void epicsRunDbTests(void) runTest(callbackTest); runTest(dbStateTest); runTest(dbShutdownTest); + runTest(scanIoTest); runTest(dbLockTest); runTest(dbPutLinkTest); runTest(testDbChannel); diff --git a/src/ioc/db/test/scanIoTest.c b/src/ioc/db/test/scanIoTest.c new file mode 100644 index 000000000..b4d2b0d25 --- /dev/null +++ b/src/ioc/db/test/scanIoTest.c @@ -0,0 +1,519 @@ +/*************************************************************************\ +* Copyright (c) 2013 UChicago Argonne LLC, as Operator of Argonne +* National Laboratory. +* Copyright (c) 2013 Helmholtz-Zentrum Berlin +* für Materialien und Energie GmbH. +* EPICS BASE is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +/* + * Author: Ralph Lange + */ + +#include + +#include "epicsEvent.h" +#include "epicsMessageQueue.h" +#include "epicsPrint.h" +#include "epicsMath.h" +#include "alarm.h" +#include "menuPriority.h" +#include "dbChannel.h" +#include "dbStaticLib.h" +#include "dbAccessDefs.h" +#include "dbScan.h" +#include "dbLock.h" +#include "dbUnitTest.h" +#include "dbCommon.h" +#include "registry.h" +#include "registryRecordType.h" +#include "registryDeviceSupport.h" +#include "recSup.h" +#include "devSup.h" +#include "iocInit.h" +#include "callback.h" +#include "ellLib.h" +#include "epicsUnitTest.h" +#include "testMain.h" +#include "osiFileName.h" + +#define GEN_SIZE_OFFSET +#include "yRecord.h" + +#include "epicsExport.h" + +#define ONE_THREAD_LOOPS 101 +#define PAR_THREAD_LOOPS 53 +#define CB_THREAD_LOOPS 13 + +#define NO_OF_THREADS 7 +#define NO_OF_MEMBERS 5 +#define NO_OF_GROUPS 11 + +#define NO_OF_MID_THREADS 3 + +static int noOfGroups = NO_OF_GROUPS; +static int noOfIoscans = NO_OF_GROUPS; + +static IOSCANPVT *ioscanpvt; /* Soft interrupt sources */ +static ELLLIST *pvtList; /* Per group private part lists */ + +static int executionOrder; +static int orderFail; +static int testNo; +static epicsMessageQueueId *mq; /* Per group message queue */ +static epicsEventId *barrier; /* Per group barrier event */ +static int *cbCounter; + +struct pvtY { + ELLNODE node; + yRecord *prec; + int group; + int member; + int count; + int processed; + int callback; +}; + +/* test2: priority and ioscan index for each group + * used priorities are expressed in the bit pattern of (ioscan index + 1) */ +struct groupItem { + int prio; + int ioscan; +} groupTable[12] = { + { 0, 0 }, + { 1, 1 }, + { 0, 2 }, { 1, 2 }, + { 2, 3 }, + { 0, 4 }, { 2, 4 }, + { 1, 5 }, { 2, 5 }, + { 0, 6 }, { 1, 6 }, { 2, 6 } +}; +static int recsProcessed = 1; +static int noDoubleCallback = 1; + +void scanIoTest_registerRecordDeviceDriver(struct dbBase *); + +long count_bits(long n) { + unsigned int c; // c accumulates the total bits set in v + for (c = 0; n; c++) + n &= n - 1; // clear the least significant bit set + return c; +} + +/*************************************************************************\ +* yRecord: minimal record needed to test I/O Intr scanning +\*************************************************************************/ + +static long get_ioint_info(int cmd, yRecord *prec, IOSCANPVT *ppvt) +{ + struct pvtY *pvt = (struct pvtY *)(prec->dpvt); + + if (testNo == 2) + *ppvt = ioscanpvt[groupTable[pvt->group].ioscan]; + else + *ppvt = ioscanpvt[pvt->group]; + return 0; +} + +struct ydset { + long number; + DEVSUPFUN report; + DEVSUPFUN init; + DEVSUPFUN init_record; + DEVSUPFUN get_ioint_info; + DEVSUPFUN process; +} devY = { + 5, + NULL, + NULL, + NULL, + get_ioint_info, + NULL +}; +epicsExportAddress(dset, devY); + +static long init_record(yRecord *prec, int pass) +{ + struct pvtY *pvt; + + if (pass == 0) return 0; + + pvt = (struct pvtY *) calloc(1, sizeof(struct pvtY)); + prec->dpvt = pvt; + + pvt->prec = prec; + sscanf(prec->name, "g%dm%d", &pvt->group, &pvt->member); + ellAdd(&pvtList[pvt->group], &pvt->node); + + return 0; +} + +static long process(yRecord *prec) +{ + struct pvtY *pvt = (struct pvtY *)(prec->dpvt); + + if (testNo == 0) { + // Single callback thread + if (executionOrder != pvt->member) { + orderFail = 1; + } + pvt->count++; + if (++executionOrder == NO_OF_MEMBERS) executionOrder = 0; + } else { + pvt->count++; + if (pvt->member == 0) { + epicsMessageQueueSend(mq[pvt->group], NULL, 0); + epicsEventMustWait(barrier[pvt->group]); + } + } + pvt->processed = 1; + return 0; +} + +rset yRSET={ + 4, + NULL, //report, + NULL, //initialize, + init_record, + process +}; +epicsExportAddress(rset, yRSET); + +static void startMockIoc(void) { + char substitutions[256]; + int i, j; + char *prio[] = { "LOW", "MEDIUM", "HIGH" }; + + if (testNo == 2) { + noOfGroups = 12; + noOfIoscans = 7; + } + ioscanpvt = calloc(noOfIoscans, sizeof(IOSCANPVT)); + mq = calloc(noOfGroups, sizeof(epicsMessageQueueId)); + barrier = calloc(noOfGroups, sizeof(epicsEventId)); + pvtList = calloc(noOfGroups, sizeof(ELLLIST)); + cbCounter = calloc(noOfGroups, sizeof(int)); + + if (dbReadDatabase(&pdbbase, "scanIoTest.dbd", + "." OSI_PATH_LIST_SEPARATOR ".." OSI_PATH_LIST_SEPARATOR + "../O.Common" OSI_PATH_LIST_SEPARATOR "O.Common", NULL)) + testAbort("Error reading database description 'scanIoTest.dbd'"); + + callbackParallelThreads(1, "Low"); + callbackParallelThreads(NO_OF_MID_THREADS, "Medium"); + callbackParallelThreads(NO_OF_THREADS, "High"); + + for (i = 0; i < noOfIoscans; i++) { + scanIoInit(&ioscanpvt[i]); + } + + for (i = 0; i < noOfGroups; i++) { + mq[i] = epicsMessageQueueCreate(NO_OF_MEMBERS, 1); + barrier[i] = epicsEventMustCreate(epicsEventEmpty); + ellInit(&pvtList[i]); + } + + scanIoTest_registerRecordDeviceDriver(pdbbase); + for (i = 0; i < noOfGroups; i++) { + for (j = 0; j < NO_OF_MEMBERS; j++) { + sprintf(substitutions, "GROUP=%d,MEMBER=%d,PRIO=%s", i, j, + testNo==0?"LOW":(testNo==1?"HIGH":prio[groupTable[i].prio])); + if (dbReadDatabase(&pdbbase, "scanIoTest.db", + "." OSI_PATH_LIST_SEPARATOR "..", substitutions)) + testAbort("Error reading test database 'scanIoTest.db'"); + } + } + + testIocInitOk(); +} + +static void stopMockIoc(void) { + int i; + + testIocShutdownOk(); + epicsThreadSleep(0.1); + for (i = 0; i < noOfGroups; i++) { + epicsMessageQueueDestroy(mq[i]); mq[i] = NULL; + epicsEventDestroy(barrier[i]); barrier[i] = NULL; + ellFree(&pvtList[i]); + } + free(mq); + free(barrier); + free(pvtList); + free(cbCounter); + testdbCleanup(); +} + +static void checkProcessed(void *user, IOSCANPVT ioscan, int prio) { + struct pvtY *pvt; + int group = -1; + int i; + + for (i = 0; i < noOfGroups; i++) { + if (ioscanpvt[groupTable[i].ioscan] == ioscan + && groupTable[i].prio == prio) { + group = i; + break; + } + } + if (group == -1) + testAbort("invalid ioscanpvt in scanio callback"); + + cbCounter[group]++; + for (pvt = (struct pvtY *)ellFirst(&pvtList[group]); + pvt; + pvt = (struct pvtY *)ellNext(&pvt->node)) { + if (pvt->callback == 1) { + testDiag("callback for rec %s arrived twice\n", pvt->prec->name); + noDoubleCallback = 0; + } + if (pvt->processed == 0) { + testDiag("rec %s was not processed\n", pvt->prec->name); + recsProcessed = 0; + } + pvt->callback = 1; + } +} + +/*************************************************************************\ +* scanIoTest: Test I/O Intr scanning +* including parallel callback threads and scanio callbacks +\*************************************************************************/ + +MAIN(scanIoTest) +{ + int i, j; + int loop; + int max_one, max_one_all; + int parallel, parallel_all; + int result; + int cbCountOk; + long waiting; + struct pvtY *pvt; + + testPlan(10); + + if (noOfGroups < NO_OF_THREADS) + testAbort("ERROR: This test requires number of ioscan sources >= number of parallel threads"); + + /**************************************\ + * Single callback thread + \**************************************/ + + testNo = 0; + startMockIoc(); + + testDiag("Testing single callback thread"); + testDiag(" using %d ioscan sources, %d records for each, and %d loops", + noOfGroups, NO_OF_MEMBERS, ONE_THREAD_LOOPS); + + for (j = 0; j < ONE_THREAD_LOOPS; j++) { + for (i = 0; i < noOfIoscans; i++) { + scanIoRequest(ioscanpvt[i]); + } + } + + epicsThreadSleep(0.1); + + testOk((orderFail==0), "No out-of-order processing"); + + result = 1; + for (i = 0; i < noOfGroups; i++) { + for (pvt = (struct pvtY *)ellFirst(&pvtList[i]); + pvt; + pvt = (struct pvtY *)ellNext(&pvt->node)) { + if (pvt->count != ONE_THREAD_LOOPS) result = 0; + } + } + + testOk(result, "All per-record process counters match number of loops"); + + stopMockIoc(); + + /**************************************\ + * Multiple parallel callback threads + \**************************************/ + + testNo = 1; + startMockIoc(); + + testDiag("Testing multiple parallel callback threads"); + testDiag(" using %d ioscan sources, %d records for each, %d loops, and %d parallel threads", + noOfIoscans, NO_OF_MEMBERS, PAR_THREAD_LOOPS, NO_OF_THREADS); + + for (j = 0; j < PAR_THREAD_LOOPS; j++) { + for (i = 0; i < noOfIoscans; i++) { + scanIoRequest(ioscanpvt[i]); + } + } + + /* With parallel cb threads, order and distribution to threads are not guaranteed. + * We have stop barrier events for each request (in the first record). + * Test schedule: + * - After the requests have been put in the queue, NO_OF_THREADS threads should have taken + * one request each. + * - Each barrier event is given PAR_THREAD_LOOPS times. + * - Whenever things stop, there should be four threads waiting, one request each. + * - After all loops, each record should have processed PAR_THREAD_LOOPS times. + */ + + max_one_all = 1; + parallel_all = 1; + + for (loop = 0; loop < (PAR_THREAD_LOOPS * noOfGroups) / NO_OF_THREADS + 1; loop++) { + max_one = 1; + parallel = 0; + waiting = 0; + j = 0; + do { + epicsThreadSleep(0.001); + j++; + for (i = 0; i < noOfGroups; i++) { + int l = epicsMessageQueuePending(mq[i]); + while (epicsMessageQueueTryReceive(mq[i], NULL, 0) != -1); + if (l == 1) { + waiting |= 1 << i; + } else if (l > 1) { + max_one = 0; + } + } + parallel = count_bits(waiting); + } while (j < 5 && parallel < NO_OF_THREADS); + + if (!max_one) max_one_all = 0; + if (loop < (PAR_THREAD_LOOPS * noOfGroups) / NO_OF_THREADS) { + if (!(parallel == NO_OF_THREADS)) parallel_all = 0; + } else { + /* In the last run of the loop only the remaining requests are processed */ + if (!(parallel == PAR_THREAD_LOOPS * noOfGroups % NO_OF_THREADS)) parallel_all = 0; + } + + for (i = 0; i < noOfGroups; i++) { + if (waiting & (1 << i)) { + epicsEventTrigger(barrier[i]); + } + } + } + + testOk(max_one_all, "No thread took more than one request per loop"); + testOk(parallel_all, "Correct number of requests were being processed in parallel in each loop"); + + epicsThreadSleep(0.1); + + result = 1; + for (i = 0; i < noOfGroups; i++) { + for (pvt = (struct pvtY *)ellFirst(&pvtList[i]); + pvt; + pvt = (struct pvtY *)ellNext(&pvt->node)) { + if (pvt->count != PAR_THREAD_LOOPS) { + testDiag("Process counter for record %s (%d) does not match loop count (%d)", + pvt->prec->name, pvt->count, PAR_THREAD_LOOPS); + result = 0; + } + } + } + + testOk(result, "All per-record process counters match number of loops"); + + stopMockIoc(); + + /**************************************\ + * Scanio callback mechanism + \**************************************/ + + testNo = 2; + startMockIoc(); + + for (i = 0; i < noOfIoscans; i++) { + scanIoSetComplete(ioscanpvt[i], checkProcessed, NULL); + } + + testDiag("Testing scanio callback mechanism"); + testDiag(" using %d ioscan sources, %d records for each, %d loops, and 1 LOW / %d MEDIUM / %d HIGH parallel threads", + noOfIoscans, NO_OF_MEMBERS, CB_THREAD_LOOPS, NO_OF_MID_THREADS, NO_OF_THREADS); + + result = 1; + for (j = 0; j < CB_THREAD_LOOPS; j++) { + for (i = 0; i < noOfIoscans; i++) { + int prio_used; + prio_used = scanIoRequest(ioscanpvt[i]); + if (i+1 != prio_used) + result = 0; + } + } + testOk(result, "All requests return the correct priority callback mask (all 7 permutations covered)"); + + /* Test schedule: + * After the requests have been put in the queue, it is checked + * - that each callback arrives exactly once, + * - after all records in the group have been processed. + */ + + /* loop count times 4 since (worst case) one loop triggers 4 groups for the single LOW thread */ + for (loop = 0; loop < CB_THREAD_LOOPS * 4; loop++) { + max_one = 1; + parallel = 0; + waiting = 0; + j = 0; + do { + epicsThreadSleep(0.001); + j++; + for (i = 0; i < noOfGroups; i++) { + int l = epicsMessageQueuePending(mq[i]); + while (epicsMessageQueueTryReceive(mq[i], NULL, 0) != -1); + if (l == 1) { + waiting |= 1 << i; + } else if (l > 1) { + max_one = 0; + } + } + parallel = count_bits(waiting); + } while (j < 5); +\ + for (i = 0; i < noOfGroups; i++) { + if (waiting & (1 << i)) { + for (pvt = (struct pvtY *)ellFirst(&pvtList[i]); + pvt; + pvt = (struct pvtY *)ellNext(&pvt->node)) { + pvt->processed = 0; + pvt->callback = 0; + /* record processing will set this at the end of process() */ + } + epicsEventTrigger(barrier[i]); + } + } + } + + epicsThreadSleep(0.1); + + testOk(recsProcessed, "Each callback occured after all records in the group were processed"); + testOk(noDoubleCallback, "No double callbacks occured in any loop"); + + result = 1; + cbCountOk = 1; + for (i = 0; i < noOfGroups; i++) { + if (cbCounter[i] != CB_THREAD_LOOPS) { + testDiag("Callback counter for group %d (%d) does not match loop count (%d)", + i, cbCounter[i], CB_THREAD_LOOPS); + cbCountOk = 0; + } + for (pvt = (struct pvtY *)ellFirst(&pvtList[i]); + pvt; + pvt = (struct pvtY *)ellNext(&pvt->node)) { + if (pvt->count != CB_THREAD_LOOPS) { + testDiag("Process counter for record %s (%d) does not match loop count (%d)", + pvt->prec->name, pvt->count, CB_THREAD_LOOPS); + result = 0; + } + } + } + + testOk(result, "All per-record process counters match number of loops"); + testOk(cbCountOk, "All per-group callback counters match number of loops"); + + stopMockIoc(); + + return testDone(); +} diff --git a/src/ioc/db/test/scanIoTest.db b/src/ioc/db/test/scanIoTest.db new file mode 100644 index 000000000..810a84edf --- /dev/null +++ b/src/ioc/db/test/scanIoTest.db @@ -0,0 +1,4 @@ +record(y, g$(GROUP)m$(MEMBER)) { + field(SCAN, "I/O Intr") + field(PRIO, "$(PRIO)") +} diff --git a/src/ioc/db/test/yRecord.dbd b/src/ioc/db/test/yRecord.dbd new file mode 100644 index 000000000..0aa970da5 --- /dev/null +++ b/src/ioc/db/test/yRecord.dbd @@ -0,0 +1,10 @@ +# This is a minimal I/O scanned record + +recordtype(y) { + include "dbCommon.dbd" + field(VAL, DBF_LONG) { + prompt("Value") + } +} + +device(y,CONSTANT,devY,"ScanIO Test")