From c042b08ab062df1eda71aad141a81ba2145d2913 Mon Sep 17 00:00:00 2001 From: Jure Varlec Date: Tue, 10 May 2022 18:42:05 +0200 Subject: [PATCH] Extend timestamp filter, giving access to the record timestamp --- modules/database/src/ioc/db/db_field_log.h | 17 +- .../database/src/std/filters/filters.dbd.pod | 92 +++++- modules/database/src/std/filters/ts.c | 311 +++++++++++++++++- modules/database/test/std/filters/tsTest.c | 265 ++++++++++++--- 4 files changed, 608 insertions(+), 77 deletions(-) diff --git a/modules/database/src/ioc/db/db_field_log.h b/modules/database/src/ioc/db/db_field_log.h index 6f57859e2..87fd238d4 100644 --- a/modules/database/src/ioc/db/db_field_log.h +++ b/modules/database/src/ioc/db/db_field_log.h @@ -39,12 +39,17 @@ extern "C" { * will adjust automatically, it just compares field sizes. */ union native_value { - epicsInt8 dbf_char; - epicsInt16 dbf_short; - epicsEnum16 dbf_enum; - epicsInt32 dbf_long; - epicsFloat32 dbf_float; - epicsFloat64 dbf_double; + epicsInt8 dbf_char; + epicsUInt8 dbf_uchar; + epicsInt16 dbf_short; + epicsUInt16 dbf_ushort; + epicsEnum16 dbf_enum; + epicsInt32 dbf_long; + epicsUInt32 dbf_ulong; + epicsInt64 dbf_int64; + epicsUInt64 dbf_uint64; + epicsFloat32 dbf_float; + epicsFloat64 dbf_double; #ifdef DB_EVENT_LOG_STRINGS char dbf_string[MAX_STRING_SIZE]; #endif diff --git a/modules/database/src/std/filters/filters.dbd.pod b/modules/database/src/std/filters/filters.dbd.pod index 7f72ebe6c..5f727a959 100644 --- a/modules/database/src/std/filters/filters.dbd.pod +++ b/modules/database/src/std/filters/filters.dbd.pod @@ -216,22 +216,96 @@ registrar(tsInitialize) =head3 TimeStamp Filter C<"ts"> -This filter replaces the timestamp in the data fetched through the channel with -the time the value was fetched (or an update was sent). The record's timestamp +This filter is used for two purposes: + +=over + +=item * to retrieve the timestamp of the record as a value in several different + formats; + +=item * to retrieve the record value as normal, but replace the timestamp with + the time the value was fetched. + +=back + +=head4 Parameters + +=head4 No parameters (an empty pair of braces) + +Retrieve the record value as normal, but replace the timestamp with the time the +value was fetched (or an update was sent). This is useful for clients that can't +handle timestamps that are far in the past. Normally, the record's timestamp indicates when the record last processed, which could have been days or even weeks ago for some records, or set to the EPICS epoch if the record has never processed. -=head4 Parameters +=head4 Numeric type C<"num"> -None, use an empty pair of braces. +The following values are accepted for this parameter: -=head4 Example +=over - Hal$ caget -a 'test:channel.{"ts":{}}' - test:channel.{"ts":{}} 2012-08-28 22:10:31.192547 0 UDF INVALID - Hal$ caget -a 'test:channel' - test:channel 0 UDF INVALID +=item * C<"dbl"> requests the timestamp as C representing the + non-integral number of seconds since epoch. This format is convenient, + but loses precision, depending on which epoch is used. + +=item * C<"sec"> requests the number of seconds since epoch as C. + +=item * C<"nsec"> requests the number of nanoseconds since epoch as + C. + +=item * C<"ts"> requests the entire timestamp. It is provided as a two-element + array of C representing seconds and nanoseconds. + +=back + +Note that C cannot be transferred over Channel Access; in that +case, the value will be converted to C. + +=head4 String type C<"str"> + +The following values are accepted for this parameter: + +=over + +=item * C<"epics"> requests the timestamp as a string in the format used by + tools such as C. + +=item * C<"iso"> requests the timestamp as a string in the ISO8601 format. + +=back + +=head4 Epoch adjustment C<"epoch"> + +The following values are accepted for this parameter: + +=over + +=item * C<"epics"> keeps the EPICS epoch (1990-01-01) and is the default if the + C<"epoch"> parameter is not specified. + +=item * C<"unix"> converts the timestamp to the UNIX/POSIX epoch (1970-01-01). + +=back + +=head4 Examples + + Hal$ caget -a 'test:invalid_ts.{"ts":{}}' + test:invalid_ts.{"ts":{}} 2012-08-28 22:10:31.192547 0 UDF INVALID + Hal$ caget -a 'test:invalid_ts' + test:invalid_ts 0 UDF INVALID + Hal$ caget -a test:channel + test:channel 2021-03-11 18:23:48.265386 42 + Hal$ caget 'test:channel.{"ts": {"str": "epics"}}' + test:channel.{"ts": {"str": "epics"}} 2021-03-11 18:23:48.265386 + Hal$ caget 'test:channel.{"ts": {"str": "iso"}}' + test:channel.{"ts": {"str": "iso"}} 2021-03-11T18:23:48.265386+0100 + Hal$ caget -f9 'test:channel.{"ts": {"num": "dbl"}}' + test:channel.{"ts": {"num": "dbl"}} 984331428.265386105 + Hal$ caget -f1 'test:channel.{"ts": {"num": "ts"}}' + test:channel.{"ts": {"num": "ts"}} 2 984331428.0 265386163.0 + Hal$ caget -f1 'test:channel.{"ts": {"num": "ts", "epoch": "unix"}}' + test:channel.{"ts": {"num": "ts", "epoch": "unix"}} 2 1615483428.0 265386163.0 =cut diff --git a/modules/database/src/std/filters/ts.c b/modules/database/src/std/filters/ts.c index 0a0a4d3ba..b98f3e19c 100644 --- a/modules/database/src/std/filters/ts.c +++ b/modules/database/src/std/filters/ts.c @@ -1,4 +1,5 @@ /*************************************************************************\ +* Copyright (c) 2021 Cosylab d.d * Copyright (c) 2010 Brookhaven National Laboratory. * Copyright (c) 2010 Helmholtz-Zentrum Berlin * fuer Materialien und Energie GmbH. @@ -9,6 +10,7 @@ /* * Author: Ralph Lange + * Author: Jure Varlec */ #include @@ -16,20 +18,122 @@ #include #include "chfPlugin.h" -#include "db_field_log.h" #include "dbExtractArray.h" #include "dbLock.h" +#include "db_field_log.h" #include "epicsExport.h" +#include "freeList.h" +#include "cantProceed.h" -/* - * The size of the data is different for each channel, and can even - * change at runtime, so a freeList doesn't make much sense here. - */ +/* Allocation size for freelists */ +#define ALLOC_NUM_ELEMENTS 32 + + +/* Filter settings */ + +enum tsMode { + tsModeInvalid = 0, + tsModeGenerate = 1, + tsModeDouble = 2, + tsModeSec = 3, + tsModeNsec = 4, + tsModeArray = 5, + tsModeString = 6, +}; + +static const chfPluginEnumType ts_numeric_enum[] = { + {"dbl", 2}, {"sec", 3}, {"nsec", 4}, {"ts", 5}}; + +enum tsEpoch { + tsEpochEpics = 0, + tsEpochUnix = 1, +}; + +static const chfPluginEnumType ts_epoch_enum[] = {{"epics", 0}, {"unix", 1}}; + +enum tsString { + tsStringInvalid = 0, + tsStringEpics = 1, + tsStringIso = 2, +}; + +static const chfPluginEnumType ts_string_enum[] = {{"epics", 1}, {"iso", 2}}; + +typedef struct tsPrivate { + enum tsMode mode; + enum tsEpoch epoch; + enum tsString str; +} tsPrivate; + +static const chfPluginArgDef ts_args[] = { + chfEnum(tsPrivate, mode, "num", 0, 0, ts_numeric_enum), + chfEnum(tsPrivate, epoch, "epoch", 0, 0, ts_epoch_enum), + chfEnum(tsPrivate, str, "str", 0, 0, ts_string_enum), + chfPluginArgEnd +}; + +static int parse_finished(void *pvt) { + tsPrivate *settings = (tsPrivate *)pvt; + if (settings->str != tsStringInvalid) { + settings->mode = tsModeString; +#if defined _MSC_VER && _MSC_VER <= 1700 + // VS 2012 crashes in ISO mode, doesn't support timezones + if (settings->str == tsStringIso) { + return -1; + } +#endif + } else if (settings->mode == tsModeInvalid) { + settings->mode = tsModeGenerate; + } + return 0; +} + + +/* Allocation of filter settings */ +static void *private_free_list; + +static void * allocPvt() { + return freeListCalloc(private_free_list); +} + +static void freePvt(void *pvt) { + freeListFree(private_free_list, pvt); +} + + +/* Allocation of two-element arrays for second+nanosecond pairs */ +static void *ts_array_free_list; + +static void *allocTsArray() { + return freeListCalloc(ts_array_free_list); +} + +static void freeTsArray(db_field_log *pfl) { + freeListFree(ts_array_free_list, pfl->u.r.field); +} + +/* Allocation of strings */ +static void *string_free_list; + +static void *allocString() { + return freeListCalloc(string_free_list); +} + +static void freeString(db_field_log *pfl) { + freeListFree(string_free_list, pfl->u.r.field); +} + + +/* The dtor for waveform data for the case when we have to copy it. */ static void freeArray(db_field_log *pfl) { + /* + * The size of the data is different for each channel, and can even + * change at runtime, so a freeList doesn't make much sense here. + */ free(pfl->u.r.field); } -static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) { +static db_field_log* generate(void* pvt, dbChannel *chan, db_field_log *pfl) { epicsTimeStamp now; epicsTimeGetCurrent(&now); @@ -44,7 +148,7 @@ static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) { dbScanLock(dbChannelRecord(chan)); dbChannelGetArrayInfo(chan, &pSource, &nSource, &offset); dbExtractArray(pSource, pTarget, pfl->field_size, - nSource, pfl->no_elements, offset, 1); + nSource, pfl->no_elements, offset, 1); pfl->u.r.field = pTarget; pfl->dtor = freeArray; pfl->u.r.pvt = pvt; @@ -56,34 +160,207 @@ static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) { return pfl; } -static void channelRegisterPre(dbChannel *chan, void *pvt, - chPostEventFunc **cb_out, void **arg_out, db_field_log *probe) -{ +static db_field_log *replace_fl_value(tsPrivate const *pvt, + db_field_log *pfl, + void (*func)(tsPrivate const *, + db_field_log *)) { + /* Get rid of the old value */ + if (pfl->type == dbfl_type_ref && pfl->u.r.dtor) { + pfl->u.r.dtor(pfl); + } + pfl->no_elements = 1; + pfl->type = dbfl_type_val; + + func(pvt, pfl); + return pfl; +} + +static void ts_to_array(tsPrivate const *settings, + epicsTimeStamp const *ts, + epicsUInt32 arr[2]) { + arr[0] = ts->secPastEpoch; + arr[1] = ts->nsec; + if (settings->epoch == tsEpochUnix) { + /* Cannot use epicsTimeToWhatever because Whatever uses signed ints */ + arr[0] += POSIX_TIME_AT_EPICS_EPOCH; + } +} + +static void ts_seconds(tsPrivate const *settings, db_field_log *pfl) { + epicsUInt32 arr[2]; + ts_to_array(settings, &pfl->time, arr); + pfl->field_type = DBF_ULONG; + pfl->field_size = sizeof(epicsUInt32); + pfl->u.v.field.dbf_ulong = arr[0]; +} + +static void ts_nanos(tsPrivate const *settings, db_field_log *pfl) { + epicsUInt32 arr[2]; + ts_to_array(settings, &pfl->time, arr); + pfl->field_type = DBF_ULONG; + pfl->field_size = sizeof(epicsUInt32); + pfl->u.v.field.dbf_ulong = arr[1]; +} + +static void ts_double(tsPrivate const *settings, db_field_log *pfl) { + epicsUInt32 arr[2]; + ts_to_array(settings, &pfl->time, arr); + pfl->field_type = DBF_DOUBLE; + pfl->field_size = sizeof(epicsFloat64); + pfl->u.v.field.dbf_double = arr[0] + arr[1] * 1e-9; +} + +static void ts_array(tsPrivate const *settings, db_field_log *pfl) { + pfl->field_type = DBF_ULONG; + pfl->field_size = sizeof(epicsUInt32); + pfl->no_elements = 2; + pfl->type = dbfl_type_ref; + pfl->u.r.pvt = NULL; + pfl->u.r.field = allocTsArray(); + pfl->u.r.dtor = freeTsArray; + ts_to_array(settings, &pfl->time, (epicsUInt32*)pfl->u.r.field); +} + +static void ts_string(tsPrivate const *settings, db_field_log *pfl) { + char const *fmt; + char *field; + size_t n; + + pfl->field_type = DBF_STRING; + pfl->field_size = MAX_STRING_SIZE; + pfl->type = dbfl_type_ref; + pfl->u.r.pvt = NULL; + pfl->u.r.field = allocString(); + pfl->u.r.dtor = freeString; + + switch (settings->str) { + case tsStringEpics: + fmt = "%Y-%m-%d %H:%M:%S.%06f"; + break; + case tsStringIso: + fmt = "%Y-%m-%dT%H:%M:%S.%06f%z"; + break; + case tsStringInvalid: + default: + fmt = ""; // Silence compiler warning. + cantProceed("Logic error: invalid state encountered in ts filter"); + } + + field = (char *)pfl->u.r.field; + n = epicsTimeToStrftime(field, MAX_STRING_SIZE, fmt, &pfl->time); + if (!n) { + field[0] = 0; + } +} + +static db_field_log *filter(void *pvt, dbChannel *chan, db_field_log *pfl) { + tsPrivate *settings = (tsPrivate *)pvt; + + switch (settings->mode) { + case tsModeDouble: + return replace_fl_value(pvt, pfl, ts_double); + case tsModeSec: + return replace_fl_value(pvt, pfl, ts_seconds); + case tsModeNsec: + return replace_fl_value(pvt, pfl, ts_nanos); + case tsModeArray: + return replace_fl_value(pvt, pfl, ts_array); + case tsModeString: + return replace_fl_value(pvt, pfl, ts_string); + case tsModeGenerate: + case tsModeInvalid: + cantProceed("Logic error: invalid state encountered in ts filter"); + } + + return pfl; +} + + +/* Only the "generate" mode is registered for the pre-queue chain as it creates + it's own timestamp which should be as close to the event as possible */ +static void channelRegisterPre(dbChannel * chan, void *pvt, + chPostEventFunc **cb_out, void **arg_out, + db_field_log *probe) { + tsPrivate *settings = (tsPrivate *)pvt; + *cb_out = settings->mode == tsModeGenerate ? generate : NULL; +} + +/* For other modes, the post-chain is fine as they only manipulate existing + timestamps */ +static void channelRegisterPost(dbChannel *chan, void *pvt, + chPostEventFunc **cb_out, void **arg_out, + db_field_log *probe) { + tsPrivate *settings = (tsPrivate *)pvt; + + if (settings->mode == tsModeGenerate || settings->mode == tsModeInvalid) { + *cb_out = NULL; + return; + } + *cb_out = filter; + *arg_out = pvt; + + /* Get rid of the value of the probe because we will be changing the + datatype */ + if (probe->type == dbfl_type_ref && probe->u.r.dtor) { + probe->u.r.dtor(probe); + } + probe->no_elements = 1; + probe->type = dbfl_type_val; + + switch (settings->mode) { + case tsModeArray: + probe->no_elements = 2; + /* fallthrough */ + case tsModeSec: + case tsModeNsec: + probe->field_type = DBF_ULONG; + probe->field_size = sizeof(epicsUInt32); + break; + case tsModeDouble: + probe->field_type = DBF_DOUBLE; + probe->field_size = sizeof(epicsFloat64); + break; + case tsModeString: + probe->field_type = DBF_STRING; + probe->field_size = MAX_STRING_SIZE; + break; + case tsModeGenerate: + case tsModeInvalid: + cantProceed("Logic error: invalid state encountered in ts filter"); + } } static void channel_report(dbChannel *chan, void *pvt, int level, const unsigned short indent) { - printf("%*sTimestamp (ts)\n", indent, ""); + tsPrivate *settings = (tsPrivate *)pvt; + printf("%*sTimestamp (ts): mode: %d, epoch: %d, str: %d\n", + indent, "", settings->mode, settings->epoch, settings->str); } static chfPluginIf pif = { - NULL, /* allocPvt, */ - NULL, /* freePvt, */ + allocPvt, + freePvt, - NULL, /* parse_error, */ - NULL, /* parse_ok, */ + NULL, /* parse_error, */ + parse_finished, NULL, /* channel_open, */ channelRegisterPre, - NULL, /* channelRegisterPost, */ + channelRegisterPost, channel_report, NULL /* channel_close */ }; static void tsInitialize(void) { - chfPluginRegister("ts", &pif, NULL); + freeListInitPvt(&private_free_list, sizeof(tsPrivate), + ALLOC_NUM_ELEMENTS); + freeListInitPvt(&ts_array_free_list, 2 * sizeof(epicsUInt32), + ALLOC_NUM_ELEMENTS); + freeListInitPvt(&string_free_list, MAX_STRING_SIZE, + ALLOC_NUM_ELEMENTS); + chfPluginRegister("ts", &pif, ts_args); } epicsExportRegistrar(tsInitialize); diff --git a/modules/database/test/std/filters/tsTest.c b/modules/database/test/std/filters/tsTest.c index e26dd41d1..697d17559 100644 --- a/modules/database/test/std/filters/tsTest.c +++ b/modules/database/test/std/filters/tsTest.c @@ -1,4 +1,5 @@ /*************************************************************************\ +* Copyright (c) 2021 Cosylab d.d * Copyright (c) 2010 Brookhaven National Laboratory. * Copyright (c) 2010 Helmholtz-Zentrum Berlin * fuer Materialien und Energie GmbH. @@ -9,9 +10,11 @@ /* * Author: Ralph Lange + * Author: Jure Varlec */ #include +#include #include "dbStaticLib.h" #include "dbAccessDefs.h" @@ -25,11 +28,24 @@ #include "testMain.h" #include "osiFileName.h" +/* A fill pattern for setting a field log to something "random". */ #define PATTERN 0x55 -void filterTest_registerRecordDeviceDriver(struct dbBase *); +/* Use a "normal" timestamp for testing. What results from filling the field log + with the above pattern is a timestamp that causes problems with + epicsTimeToStrftime() on some platforms. */ +static epicsTimeStamp const test_ts = { 616600420, 998425354 }; -static db_field_log fl; +typedef int (*TypeCheck)(const db_field_log *pfl); +typedef int (*ValueCheck)(const db_field_log *pfl, const epicsTimeStamp *ts); + +typedef struct { + char const *channel; + TypeCheck type_check; + ValueCheck value_check; +} TestSpec; + +void filterTest_registerRecordDeviceDriver(struct dbBase *); static int fl_equal(const db_field_log *pfl1, const db_field_log *pfl2) { return !(memcmp(pfl1, pfl2, sizeof(db_field_log))); @@ -42,21 +58,209 @@ static int fl_equal_ex_ts(const db_field_log *pfl1, const db_field_log *pfl2) { return fl_equal(&fl1, pfl2); } -MAIN(tsTest) -{ +static void fl_reset(db_field_log *pfl) { + memset(pfl, PATTERN, sizeof(*pfl)); + pfl->time = test_ts; +} + +static void test_generate_filter(const chFilterPlugin *plug) { dbChannel *pch; chFilter *filter; - const chFilterPlugin *plug; - char ts[] = "ts"; - ELLNODE *node; - chPostEventFunc *cb_out = NULL; - void *arg_out = NULL; + db_field_log fl; db_field_log fl1; db_field_log *pfl2; epicsTimeStamp stamp, now; - dbEventCtx evtctx; + ELLNODE *node; + chPostEventFunc *cb_out = NULL; + void *arg_out = NULL; + char const *chan_name = "x.VAL{ts:{}}"; - testPlan(12); + testOk(!!(pch = dbChannelCreate(chan_name)), + "dbChannel with plugin ts created"); + testOk((ellCount(&pch->filters) == 1), "channel has one plugin"); + + fl_reset(&fl); + fl1 = fl; + node = ellFirst(&pch->filters); + filter = CONTAINER(node, chFilter, list_node); + plug->fif->channel_register_post(filter, &cb_out, &arg_out, &fl1); + plug->fif->channel_register_pre(filter, &cb_out, &arg_out, &fl1); + testOk(!!(cb_out) && !(arg_out), + "register_pre registers one filter w/o argument"); + testOk(fl_equal(&fl1, &fl), + "register_pre does not change field_log data type"); + + testOk(!(dbChannelOpen(pch)), "dbChannel with plugin ts opened"); + node = ellFirst(&pch->pre_chain); + filter = CONTAINER(node, chFilter, pre_node); + testOk((ellCount(&pch->pre_chain) == 1 && filter->pre_arg == NULL), + "ts has one filter w/o argument in pre chain"); + testOk((ellCount(&pch->post_chain) == 0), "ts has no filter in post chain"); + + fl_reset(&fl); + fl1 = fl; + pfl2 = dbChannelRunPreChain(pch, &fl1); + testOk(pfl2 == &fl1, "ts filter does not drop or replace field_log"); + testOk(fl_equal_ex_ts(&fl1, pfl2), + "ts filter does not change field_log data"); + + testOk(!!(pfl2 = db_create_read_log(pch)), "create field log from channel"); + stamp = pfl2->time; + db_delete_field_log(pfl2); + + pfl2 = dbChannelRunPreChain(pch, &fl1); + epicsTimeGetCurrent(&now); + testOk(epicsTimeDiffInSeconds(&pfl2->time, &stamp) >= 0 && + epicsTimeDiffInSeconds(&now, &pfl2->time) >= 0, + "ts filter sets time stamp to \"now\""); + + dbChannelDelete(pch); +} + +static void test_value_filter(const chFilterPlugin *plug, const char *chan_name, + TypeCheck tc_func, ValueCheck vc_func) { + dbChannel *pch; + chFilter *filter; + db_field_log fl; + db_field_log fl2; + db_field_log *pfl; + epicsTimeStamp ts; + ELLNODE *node; + chPostEventFunc *cb_out = NULL; + void *arg_out = NULL; + + testDiag("Channel %s", chan_name); + + testOk(!!(pch = dbChannelCreate(chan_name)), + "dbChannel with plugin ts created"); + testOk((ellCount(&pch->filters) == 1), "channel has one plugin"); + + fl_reset(&fl); + fl.type = dbfl_type_val; + node = ellFirst(&pch->filters); + filter = CONTAINER(node, chFilter, list_node); + plug->fif->channel_register_pre(filter, &cb_out, &arg_out, &fl); + plug->fif->channel_register_post(filter, &cb_out, &arg_out, &fl); + testOk(!!(cb_out) && arg_out, + "register_post registers one filter with argument"); + testOk(tc_func(&fl), "register_post gives correct field type"); + + testOk(!(dbChannelOpen(pch)), "dbChannel with plugin ts opened"); + node = ellFirst(&pch->post_chain); + filter = CONTAINER(node, chFilter, post_node); + testOk((ellCount(&pch->post_chain) == 1 && filter->post_arg != NULL), + "ts has one filter with argument in post chain"); + testOk((ellCount(&pch->pre_chain) == 0), "ts has no filter in pre chain"); + + fl_reset(&fl); + fl.type = dbfl_type_val; + ts = fl.time; + fl2 = fl; + pfl = dbChannelRunPostChain(pch, &fl); + testOk(pfl == &fl, "ts filter does not drop or replace field_log"); + testOk(tc_func(pfl), "ts filter gives correct field type"); + testOk((pfl->time.secPastEpoch == fl2.time.secPastEpoch && + pfl->time.nsec == fl2.time.nsec && pfl->stat == fl2.stat && + pfl->sevr == fl2.sevr), + "ts filter does not touch non-value fields of field_log"); + testOk(vc_func(pfl, &ts), "ts filter gives correct field value"); + + dbChannelDelete(pch); +} + +static int type_check_double(const db_field_log *pfl) { + return pfl->type == dbfl_type_val + && pfl->field_type == DBR_DOUBLE + && pfl->field_size == sizeof(epicsFloat64) + && pfl->no_elements == 1; +} + +static int value_check_double(const db_field_log *pfl, const epicsTimeStamp *ts) { + epicsFloat64 flt = pfl->u.v.field.dbf_double; + epicsFloat64 nsec = (flt - (epicsUInt32)(flt)) * 1e9; + return ts->secPastEpoch == (epicsUInt32)(flt) + && fabs(ts->nsec - nsec) < 1000.; /* allow loss of precision */ +} + +static int type_check_sec_nsec(const db_field_log *pfl) { + return pfl->type == dbfl_type_val + && pfl->field_type == DBR_ULONG + && pfl->field_size == sizeof(epicsUInt32) + && pfl->no_elements == 1; +} + +static int value_check_sec(const db_field_log *pfl, const epicsTimeStamp *ts) { + return ts->secPastEpoch == pfl->u.v.field.dbf_ulong; +} + +static int value_check_nsec(const db_field_log *pfl, const epicsTimeStamp *ts) { + return ts->nsec == pfl->u.v.field.dbf_ulong; +} + +static int type_check_array(const db_field_log *pfl) { + return pfl->field_type == DBR_ULONG + && pfl->field_size == sizeof(epicsUInt32) + && pfl->no_elements == 2; +} + +static int value_check_array(const db_field_log *pfl, const epicsTimeStamp *ts) { + epicsUInt32 *arr = (epicsUInt32*)pfl->u.r.field; + return pfl->type == dbfl_type_ref + && pfl->u.r.field != NULL + && pfl->u.r.dtor != NULL + && pfl->u.r.pvt == NULL + && ts->secPastEpoch == arr[0] + && ts->nsec == arr[1]; +} + +static int value_check_unix(const db_field_log *pfl, const epicsTimeStamp *ts) { + epicsUInt32 *arr = (epicsUInt32 *)pfl->u.r.field; + return pfl->type == dbfl_type_ref + && pfl->u.r.field != NULL + && pfl->u.r.dtor != NULL + && pfl->u.r.pvt == NULL + && ts->secPastEpoch == arr[0] - POSIX_TIME_AT_EPICS_EPOCH + && ts->nsec == arr[1]; +} + +static int type_check_string(const db_field_log *pfl) { + return pfl->field_type == DBR_STRING + && pfl->field_size == MAX_STRING_SIZE + && pfl->no_elements == 1; +} + +static int value_check_string(const db_field_log *pfl, const epicsTimeStamp *ts) { + /* We can only verify the type, not the value, because using strptime() + might be problematic. */ + return pfl->type == dbfl_type_ref + && pfl->u.r.field != NULL + && pfl->u.r.dtor != NULL + && pfl->u.r.pvt == NULL; +} + +MAIN(tsTest) { + int i; + char ts[] = "ts"; + dbEventCtx evtctx; + const chFilterPlugin *plug; + + static TestSpec const tests[] = { + {"x.VAL{ts:{\"num\": \"dbl\"}}", type_check_double, value_check_double}, + {"x.VAL{ts:{\"num\": \"sec\"}}", type_check_sec_nsec, value_check_sec}, + {"x.VAL{ts:{\"num\": \"nsec\"}}", type_check_sec_nsec, value_check_nsec}, + {"x.VAL{ts:{\"num\": \"ts\"}}", type_check_array, value_check_array}, + {"x.VAL{ts:{\"num\": \"ts\", \"epoch\": \"epics\"}}", type_check_array, value_check_array}, + {"x.VAL{ts:{\"num\": \"ts\", \"epoch\": \"unix\"}}", type_check_array, value_check_unix}, + {"x.VAL{ts:{\"str\": \"epics\"}}", type_check_string, value_check_string}, +#if !(defined _MSC_VER && _MSC_VER <= 1700) + {"x.VAL{ts:{\"str\": \"iso\"}}", type_check_string, value_check_string}, +#endif + }; + + static int const num_value_tests = sizeof(tests) / sizeof(tests[0]); + + testPlan(12 /* test_generate_filter() */ + + num_value_tests * 11 /* test_value_filter() */); testdbPrepare(); @@ -77,41 +281,12 @@ MAIN(tsTest) testAbort("plugin '%s' not registered", ts); testPass("plugin '%s' registered correctly", ts); - testOk(!!(pch = dbChannelCreate("x.VAL{ts:{}}")), "dbChannel with plugin ts created"); - testOk((ellCount(&pch->filters) == 1), "channel has one plugin"); + test_generate_filter(plug); - memset(&fl, PATTERN, sizeof(fl)); - fl1 = fl; - node = ellFirst(&pch->filters); - filter = CONTAINER(node, chFilter, list_node); - plug->fif->channel_register_pre(filter, &cb_out, &arg_out, &fl1); - testOk(!!(cb_out) && !(arg_out), "register_pre registers one filter w/o argument"); - testOk(fl_equal(&fl1, &fl), "register_pre does not change field_log data type"); - - testOk(!(dbChannelOpen(pch)), "dbChannel with plugin ts opened"); - node = ellFirst(&pch->pre_chain); - filter = CONTAINER(node, chFilter, pre_node); - testOk((ellCount(&pch->pre_chain) == 1 && filter->pre_arg == NULL), - "ts has one filter w/o argument in pre chain"); - testOk((ellCount(&pch->post_chain) == 0), "ts has no filter in post chain"); - - memset(&fl, PATTERN, sizeof(fl)); - fl1 = fl; - pfl2 = dbChannelRunPreChain(pch, &fl1); - testOk(pfl2 == &fl1, "ts filter does not drop or replace field_log"); - testOk(fl_equal_ex_ts(&fl1, pfl2), "ts filter does not change field_log data"); - - testOk(!!(pfl2 = db_create_read_log(pch)), "create field log from channel"); - stamp = pfl2->time; - db_delete_field_log(pfl2); - - pfl2 = dbChannelRunPreChain(pch, &fl1); - epicsTimeGetCurrent(&now); - testOk(epicsTimeDiffInSeconds(&pfl2->time, &stamp) >= 0 && - epicsTimeDiffInSeconds(&now, &pfl2->time) >= 0, - "ts filter sets time stamp to \"now\""); - - dbChannelDelete(pch); + for (i = 0; i < num_value_tests; ++i) { + TestSpec const *t = &tests[i]; + test_value_filter(plug, t->channel, t->type_check, t->value_check); + } db_close_events(evtctx);