/*************************************************************************\
* 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 unitests 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 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};
 * testdbVGetFieldEqual("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 pervious 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>
 *
 * 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);
 *     testCase();
 *     testCase(); // may be repeated if desirable.
 *     return testDone();
 * }
 * @endcode
 *
 * @code
 * 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
 * @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
