diff --git a/.appveyor.yml b/.appveyor.yml index 7632b32c4..9c4590e54 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -82,8 +82,9 @@ matrix: #---------------------------------# install: + - cmd: set PATH=C:\Python38-x64;%PATH% - cmd: git submodule update --init --recursive - - cmd: pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper + - cmd: python -m pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper - cmd: python .ci/cue.py prepare build_script: @@ -121,10 +122,4 @@ on_failure: #---------------------------------# notifications: - - - provider: Email - to: - - core-talk@aps.anl.gov - on_build_success: false - - provider: GitHubPullRequest diff --git a/.appveyor/epics-base-7.yml b/.appveyor/epics-base-7.yml index 8616544e9..40c892cde 100644 --- a/.appveyor/epics-base-7.yml +++ b/.appveyor/epics-base-7.yml @@ -89,8 +89,9 @@ matrix: #---------------------------------# install: + - cmd: set PATH=C:\Python38-x64;%PATH% - cmd: git submodule update --init --recursive - - cmd: pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper + - cmd: python -m pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper - cmd: python .ci/cue.py prepare build_script: @@ -128,10 +129,4 @@ on_failure: #---------------------------------# notifications: - - - provider: Email - to: - - core-talk@aps.anl.gov - on_build_success: false - - provider: GitHubPullRequest diff --git a/configure/os/CONFIG.Common.iosCommon b/configure/os/CONFIG.Common.iosCommon index 03d3cab8e..3f6dada42 100644 --- a/configure/os/CONFIG.Common.iosCommon +++ b/configure/os/CONFIG.Common.iosCommon @@ -91,11 +91,6 @@ vpath %.m $(USR_VPATH) $(ALL_SRC_DIRS) %.o: %.m $(COMPILE.c) -c $< -#-------------------------------------------------- -# Header dependency file generation -# -HDEPENDS_METHOD = MKMF - #-------------------------------------------------- # Allow site overrides -include $(CONFIG)/os/CONFIG_SITE.Common.iosCommon diff --git a/configure/os/CONFIG.Common.vxWorksCommon b/configure/os/CONFIG.Common.vxWorksCommon index f28f0e327..8f562abb6 100644 --- a/configure/os/CONFIG.Common.vxWorksCommon +++ b/configure/os/CONFIG.Common.vxWorksCommon @@ -145,12 +145,6 @@ CODE_CXXFLAGS = $(CODE_CXXFLAGS_$(VXWORKS_MAJOR_VERSION)) SHRLIB_CFLAGS = SHRLIB_LDFLAGS = -#-------------------------------------------------- -# Don't use gcc 2.x for dependency generation - -HDEPENDS_METHOD_2 = MKMF -HDEPENDS_METHOD = $(firstword $(HDEPENDS_METHOD_$(VX_GNU_MAJOR_VERSION)) COMP) - #-------------------------------------------------- # osithead use default stack, YES or NO override OSITHREAD_USE_DEFAULT_STACK = NO diff --git a/configure/os/CONFIG.darwinCommon.darwinCommon b/configure/os/CONFIG.darwinCommon.darwinCommon index 2c12b2fbb..d6a7ff11a 100644 --- a/configure/os/CONFIG.darwinCommon.darwinCommon +++ b/configure/os/CONFIG.darwinCommon.darwinCommon @@ -82,11 +82,6 @@ vpath %.m $(USR_VPATH) $(ALL_SRC_DIRS) %.o: %.m $(COMPILE.c) -c $< -# -# Header dependency file generation -# -HDEPENDS_METHOD = MKMF - #-------------------------------------------------- # Allow site overrides -include $(CONFIG)/os/CONFIG_SITE.darwinCommon.darwinCommon diff --git a/documentation/Makefile.headers b/documentation/Makefile.headers index 016af88ca..d892677e9 100644 --- a/documentation/Makefile.headers +++ b/documentation/Makefile.headers @@ -69,6 +69,7 @@ database_HEADERS += chfPlugin database_HEADERS += dbChannel database_HEADERS += dbCommon database_HEADERS += dbDefs +database_HEADERS += dbEvent database_HEADERS += dbExtractArray database_HEADERS += dbLink database_HEADERS += dbServer diff --git a/modules/database/src/ioc/db/dbEvent.h b/modules/database/src/ioc/db/dbEvent.h index 32e54265b..ed20b429f 100644 --- a/modules/database/src/ioc/db/dbEvent.h +++ b/modules/database/src/ioc/db/dbEvent.h @@ -49,17 +49,84 @@ DBCORE_API int db_post_events ( void *pRecord, void *pField, unsigned caEventMask ); typedef void EXTRALABORFUNC (void *extralabor_arg); +/** @brief Allocate event reception context + * @return NULL on error + * + * On success, call db_start_events(), and then eventually db_close_events(). + * + * @pre Call after initHookAfterInitDatabase and before initHookBeforeFree. + */ DBCORE_API dbEventCtx db_init_events (void); +/** @brief Start listener thread + * @param ctx Context + * @param taskname Thread name + * @param init_func If not NULL, call from the newly created listener thread + * @param init_func_arg Argument to init_func + * @param osiPriority Thread priority. See epicsThreadOpts::priority + * @return DB_EVENT_OK Success. DB_EVENT_ERROR, failed to create new thread. + * + * Basic lifecycle: + * + * @code{.cpp} + * dbEventCtx ctxt = db_init_events(); + * assert(ctxt); + * int ret = db_start_events(ctxt, "mymodule", NULL, NULL, 0); + * assert(ret==DB_EVENT_OK); + * ... create and close subscriptions + * db_close_events(ctxt); + * @endcode + * + * Flow control initially disabled, so events can be delivered. + */ DBCORE_API int db_start_events ( dbEventCtx ctx, const char *taskname, void (*init_func)(void *), void *init_func_arg, unsigned osiPriority ); +/** @brief Stop and deallocate event reception context + * @param ctx Context + * + * @pre Call after initHookAfterInitDatabase and before initHookBeforeFree. + * @pre All dbEventSubscription must first be deallocated by db_cancel_event(). + * @post Joins event listener thread. + */ DBCORE_API void db_close_events (dbEventCtx ctx); +/** @brief Enable flow control, pause event delivery + * @param ctx Context + */ DBCORE_API void db_event_flow_ctrl_mode_on (dbEventCtx ctx); +/** @brief Disable flow control, resume event delivery + * @param ctx Context + */ DBCORE_API void db_event_flow_ctrl_mode_off (dbEventCtx ctx); +/** @brief Setup/Clear extra labor callback + * @param ctx Context + * @param func Extra labor callback, may be NULL to clear previously set callback. + * @param arg Argument to func + * @return DB_EVENT_OK, always succeeds. + * + * Does not queue callback. See db_post_extra_labor(). + */ DBCORE_API int db_add_extra_labor_event ( dbEventCtx ctx, EXTRALABORFUNC *func, void *arg); +/** @brief Wait for extra labor callback. + * + * @pre Do not call from event listener thread. + * @post extra labor queued flag is unchanged + * + * To ensure completion, call should arrange that db_post_extra_labor() will + * not be called again. + */ DBCORE_API void db_flush_extra_labor_event (dbEventCtx); +/** @brief Queue extra labor callback event + * @param ctx Context + * @return DB_EVENT_OK, always succeeds. + * + * Sets an internal queued flag, which still be set from a previous call. + */ DBCORE_API int db_post_extra_labor (dbEventCtx ctx); +/** @brief Change event listener thread priority + * @param ctx Context + * @param epicsPriority Thread priority. See epicsThreadOpts::priority + */ DBCORE_API void db_event_change_priority ( dbEventCtx ctx, unsigned epicsPriority ); #ifdef EPICS_PRIVATE_API @@ -67,19 +134,106 @@ DBCORE_API void db_cleanup_events(void); DBCORE_API void db_init_event_freelists (void); #endif +/** @brief Subscription event callback + * @param user_arg private argument passed to db_add_event() + * @param chan dbChannel passed to db_add_event() + * @param eventsRemaining Approximate number of events remaining in queued. + * Not exact. Use is discouraged. + * @param pfl db_field_log of this event. + * + * An event callback is expected to call dbChannelGetField() or take equivalent action. + * eg. explicitly lock the record and call dbChannelGet(). + * + * Callee must _not_ delete the db_field_log. + * + * @since 7.0.5, @code pfl->mask @endcode may be used to detect which condition(s) + * triggered this event. + */ typedef void EVENTFUNC (void *user_arg, struct dbChannel *chan, int eventsRemaining, struct db_field_log *pfl); +/** @brief Create subscription to channel + * @param ctx Context + * @param chan Channel + * @param user_sub Event callback + * @param user_arg Private argument + * @param select Bit mask of DBE_VALUE and others. See caeventmask.h + * @return NULL on error. On success, later call db_cancel_event() + * + * Creates a new subscription to the specified dbChannel. + * Callbacks will be delivered on the listener thread of the provided Context. + * + * Creation does not queue any events. + * Follow with a call to db_post_single_event() to queue an initial event. + * + * Subscription is initially disabled. Call db_event_enable(); + * + * Basic lifecycle: + * + * @code{.cpp} + * static + * void mycb(void *priv, struct dbChannel *chan, int eventsRemaining, struct db_field_log *pfl) { + * (void)eventsRemaining; // use not recommended + * + * // dbChannelGetField() locks record. + * // May read value and meta-data from event (db_field_log). + * long ret = dbChannelGetField(chan, ..., pfl); + * } + * void someaction() { + * dbEventCtx ctx = ...; // previously created + * dbChannel *chan = ...; + * void *priv = ...; + * + * dbEventSubscription sub = db_add_event(ctx, chan, mycb, priv, DBE_VALUE|DBE_ALARM); + * assert(sub); + * db_cancel_event(sub); + * } + * @endcode + */ DBCORE_API dbEventSubscription db_add_event ( dbEventCtx ctx, struct dbChannel *chan, EVENTFUNC *user_sub, void *user_arg, unsigned select); +/** @brief Deallocate subscription + * @param es Subscription. Must not be NULL. + * + * Synchronizes with Event Context worker thread to wait for a concurrent callback + * to complete. + */ DBCORE_API void db_cancel_event (dbEventSubscription es); +/** @brief Immediately attempt to queue an event with the present value + * @param es Subscription + * + * Locks record and runs pre-chain of any server-side filters. + * Such a filter may drop the new event (a well designed filter should not drop the first event). + */ DBCORE_API void db_post_single_event (dbEventSubscription es); +/** @brief Enable subscription callback delivery + * @param es Subscription + */ DBCORE_API void db_event_enable (dbEventSubscription es); +/** @brief Disable subscription callback delivery + * @param es Subscription + * + * Does __not__ synchronize with listener thread, pending callbacks may be delivered. + * Use extra-labor mechanism and db_flush_extra_labor_event() to synchronize. + */ DBCORE_API void db_event_disable (dbEventSubscription es); +/** @brief Allocate subscription update event. + * @param pevent Subscription + * @return NULL on allocation failure. + */ DBCORE_API struct db_field_log* db_create_event_log (struct evSubscrip *pevent); +/** @brief Allocate "read" event. + * @param pevent Subscription + * @return NULL on allocation failure. + * + * Used by PVA or CA "GET" operations when polling the current value of a Channel. + */ DBCORE_API struct db_field_log* db_create_read_log (struct dbChannel *chan); +/** @brief db_delete_field_log + * @param pfl event structure. May be NULL (no-op). + */ DBCORE_API void db_delete_field_log (struct db_field_log *pfl); DBCORE_API int db_available_logs(void); @@ -90,4 +244,40 @@ DBCORE_API int db_available_logs(void); } #endif +/** @file dbEvent.h + * + * Internal publish/subscribe mechanism of process database. + * Direct usage is discouraged in favor of derived interfaces, + * principally local and remote PVA/CA. + * + * @since 7.0.8.1 New usage is recommended to define the USE_TYPED_DBEVENT C macro to select + * typed arguments of some calls as opposed to void pointers. + * + * @section dbeventobjects Objects + * + * - Event context (dbEventCtx) + * - Event subscription (dbEventSubscription) + * - Channel (dbChannel) + * - Event / field log (db_field_log) + * + * @section dbeventlifecycle Lifecycle + * + * Usage is tied to the lifetime of the process database. + * Either through @ref inithooks or @ref dbunittest . + * db_init_events() must not be called before initHookAfterInitDatabase or testIocInitOk(). + * db_close_events() must be called after initHookBeforeFree or testIocShutdownOk(). + * + * @note testMonitorCreate() and friends are provided to easy handling of + * subscriptions in unit tests. + * + * Subscriptions associated with an Event Context must be deallocated before + * that Context is deallocated. + * + * @section dbeventthread Concurrency + * + * Each Event Context has an associated worker thread on which subscription callbacks are invoked. + * Some API functions implicitly synchronize with that worker thread as noted. + * The "extra labor" functions may be used to explicitly synchronize with this thread. + */ + #endif /*INCLdbEventh*/ diff --git a/modules/database/src/ioc/db/dbUnitTest.h b/modules/database/src/ioc/db/dbUnitTest.h index 9f5fdae21..ba19684e0 100644 --- a/modules/database/src/ioc/db/dbUnitTest.h +++ b/modules/database/src/ioc/db/dbUnitTest.h @@ -6,13 +6,6 @@ * 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 @@ -204,15 +197,19 @@ DBCORE_API void testGlobalUnlock(void); } #endif -/** @page dbunittest Unit testing of record processing +/** @file dbUnitTest.h + * @brief Helpers for unittests of process database + * @author Michael Davidsaver, Ralph Lange * - * @see @ref epicsUnitTest.h + * @section dbunittest Unit testing of record processing + * + * @see @ref unittest * * @section dbtestskel Test skeleton * * For the impatient, the skeleton of a test: * - * @code + * @code{.c} * #include * #include * @@ -236,7 +233,7 @@ DBCORE_API void testGlobalUnlock(void); * } * @endcode * - * @code + * @code{make} * TOP = .. * include $(TOP)/configure/CONFIG * @@ -254,6 +251,27 @@ DBCORE_API void testGlobalUnlock(void); * include $(TOP)/configure/RULES * @endcode * + * 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. + * * @section dbtestactions Actions * * Several helper functions are provided to interact with a running database. @@ -276,7 +294,7 @@ DBCORE_API void testGlobalUnlock(void); * * @see enum dbfType in dbFldTypes.h * - * @code + * @code{.c} * testdbPutFieldOk("pvname", DBF_ULONG, (unsigned int)5); * testdbPutFieldOk("pvname", DBF_FLOAT, (double)4.1); * testdbPutFieldOk("pvname", DBF_STRING, "hello world"); @@ -324,7 +342,7 @@ DBCORE_API void testGlobalUnlock(void); * When possible, the best way to avoid this race would be to join the worker * before destroying the event. * - * @code + * @code{.c} * epicsEventId evt; * void thread1() { * epicsThreadOpts opts = EPICS_THREAD_OPTS_INIT; @@ -346,7 +364,7 @@ DBCORE_API void testGlobalUnlock(void); * that epicsEventMustSignal() has returned before destroying the event. * testGlobalLock() and testGlobalUnlock() provide access to such a mutex. * - * @code + * @code{.c} * epicsEventId evt; * void thread1() { * evt = epicsEventMustCreate(...); diff --git a/modules/libcom/RTEMS/epicsNtp.c b/modules/libcom/RTEMS/epicsNtp.c index 2098e2783..2c15d8a38 100644 --- a/modules/libcom/RTEMS/epicsNtp.c +++ b/modules/libcom/RTEMS/epicsNtp.c @@ -2,6 +2,8 @@ * (C) 2014 David Lettier. * http://www.lettier.com/ * SPDX-License-Identifier: BSD-3-Clause +* +* use of UDP funktions and implement timeout, HPJ 15.12.2025 \*************************************************************************/ #include @@ -20,7 +22,6 @@ int epicsNtpGetTime(char *ntpIp, struct timespec *now) { - int sockfd, n; // Socket file descriptor and the n return result from writing/reading from the socket. int portno = 123; // NTP UDP port number. @@ -30,34 +31,31 @@ int epicsNtpGetTime(char *ntpIp, struct timespec *now) // Set the first byte's bits to 00,011,011 for li = 0, vn = 3, and mode = 3. The rest will be left set to zero. *( ( char * ) &packet + 0 ) = 0x1b; // Represents 27 in base 10 or 00011011 in base 2. - // Create a UDP socket, convert the host-name to an IP address, set the port number, - // connect to the server, send the packet, and then read in the return packet. - struct sockaddr_in serv_addr; // Server address data structure. - - sockfd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); // Create a UDP socket. + /* Create an UDP socket, use IP address of given nameserver, set the port number + struct sockaddr_in serv_addr; // Server address data structure. + add timout of 2 seconds + */ + struct sockaddr_in serv_addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = inet_addr(ntpIp), + .sin_port = htons( portno ), + }; + sockfd = socket( AF_INET, SOCK_DGRAM, 0 ); if ( sockfd < 0 ) { perror( "epicsNtpGetTime creating socket" ); return -1; } - // Zero out the server address structure. - memset( ( char* ) &serv_addr, 0, sizeof( serv_addr ) ); - - serv_addr.sin_family = AF_INET; - serv_addr.sin_addr.s_addr = inet_addr(ntpIp); - - serv_addr.sin_port = htons( portno ); - - // Call up the server using its IP address and port number. - if ( connect( sockfd, ( struct sockaddr * ) &serv_addr, sizeof( serv_addr) ) < 0 ) { - perror( "epicsNtpGetTime connecting socket" ); - close( sockfd ); + /* Set receive timeout to 2 seconds */ + struct timeval tv = { .tv_sec = 2, .tv_usec = 0 }; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + perror("setsockopt"); return -1; } // Send it the NTP packet it wants. If n == -1, it failed. - n = write( sockfd, ( char* ) &packet, sizeof( ntp_packet ) ); + n = sendto(sockfd, ( char* ) &packet, sizeof( ntp_packet ), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if ( n < 0 ) { perror( "epicsNtpGetTime sending NTP request" ); close( sockfd ); @@ -65,13 +63,18 @@ int epicsNtpGetTime(char *ntpIp, struct timespec *now) } // Wait and receive the packet back from the server. If n == -1, it failed. - n = read( sockfd, ( char* ) &packet, sizeof( ntp_packet ) ); - if ( n < 0 ) { - perror( "epicsNtpGetTime reading NTP reply" ); + n = recvfrom(sockfd, ( char* ) &packet, sizeof( ntp_packet ), 0, NULL, NULL); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + printf("Timeout - no response\n"); + } else { + perror( "epicsNtpGetTime reading NTP reply" ); + } close( sockfd ); return -1; + } else { + printf("Received %zd bytes\n", n); } - close( sockfd ); // These two fields contain the time-stamp seconds as the packet left the NTP server. diff --git a/modules/libcom/src/iocsh/initHooks.h b/modules/libcom/src/iocsh/initHooks.h index 1373ca771..2febc0331 100644 --- a/modules/libcom/src/iocsh/initHooks.h +++ b/modules/libcom/src/iocsh/initHooks.h @@ -8,7 +8,9 @@ * in file LICENSE that is included with this distribution. \*************************************************************************/ -/** @page inithooks IOC lifecycle callback hooks. +/** @file initHooks.h + * + * @section inithooks IOC lifecycle callback hooks. * * initHookRegister() allows external code to be called at certains points (initHookState) * during IOC startup and shutdown. diff --git a/modules/libcom/src/misc/epicsUnitTest.h b/modules/libcom/src/misc/epicsUnitTest.h index 61c1b8239..f05df349a 100644 --- a/modules/libcom/src/misc/epicsUnitTest.h +++ b/modules/libcom/src/misc/epicsUnitTest.h @@ -11,6 +11,8 @@ * \brief Unit test routines * \author Andrew Johnson * + * @section unittest Unit test routines + * * The unit test routines make it easy for a test program to generate output * that is compatible with the Test Anything Protocol and can thus be used with * Perl's automated Test::Harness as well as generating human-readable output. @@ -72,78 +74,40 @@ * mechanism to generate its result outputs (from an epicsAtExit() callback * routine). * - * ### IOC Testing + * @see @ref dbunittest * - * 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 suggested call sequence in a test program that needs to run the - * IOC without Channel Access is: -\code -#include "iocInit.h" - -MAIN(iocTest) -{ - testPlan(0); - testdbPrepare(); - testdbReadDatabase(".dbd", 0, 0); - _registerRecordDeviceDriver(pdbbase); - testdbReadDatabase("some.db", 0, 0); - ... test code before iocInit(). eg. dbGetString() ... - testIocInitOk(); - ... test code with IOC running. eg. dbGet() - testIocShutdownOk(); - testdbCleanup(); - return testDone(); -} -\endcode - - * 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. - * - * ### Example + * @section unittestexample Example * * The following is a simple example of a test program using the epicsUnitTest * routines: -\code -#include -#include "epicsUnitTest.h" -#include "testMain.h" - -MAIN(mathTest) -{ - testPlan(3); - testOk(sin(0.0) == 0.0, "Sine starts"); - testOk(cos(0.0) == 1.0, "Cosine continues"); - if (!testOk1(M_PI == 4.0*atan(1.0))) - testDiag("4 * atan(1) = %g", 4.0 * atan(1.0)); - return testDone(); -} -\endcode - + * \code{.c} + * #include + * #include "epicsUnitTest.h" + * #include "testMain.h" + * + * MAIN(mathTest) + * { + * testPlan(3); + * testOk(sin(0.0) == 0.0, "Sine starts"); + * testOk(cos(0.0) == 1.0, "Cosine continues"); + * if (!testOk1(M_PI == 4.0*atan(1.0))) + * testDiag("4 * atan(1) = %g", 4.0 * atan(1.0)); + * return testDone(); + * } + * \endcode + * * The output from running the above program looks like this: -\code -1..3 -ok 1 - Sine starts -ok 2 - Cosine continues -ok 3 - M_PI == 4.0*atan(1.0) - - Results - ======= - Tests: 3 - Passed: 3 = 100% -\endcode + * \code + * 1..3 + * ok 1 - Sine starts + * ok 2 - Cosine continues + * ok 3 - M_PI == 4.0*atan(1.0) + * + * Results + * ======= + * Tests: 3 + * Passed: 3 = 100% + * \endcode */ #ifndef INC_epicsUnitTest_H