/* * Copyright - See the COPYRIGHT that is included with this distribution. * pvxs is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define PVXS_ENABLE_EXPERT_API #include #include #include #include #include #include #include #include "dblocker.h" #include "qsrvpvt.h" #include "pvalink.h" #include "capturestd.h" using namespace pvxs::ioc; using namespace pvxs; namespace { struct TestMonitor { testMonitor * const mon; TestMonitor(const char* pvname, unsigned dbe_mask, unsigned opt=0) :mon(testMonitorCreate(pvname, dbe_mask, opt)) {} ~TestMonitor() { testMonitorDestroy(mon); } void wait() { testMonitorWait(mon); } unsigned count(bool reset=true) { return testMonitorCount(mon, reset); } }; void testGet() { testDiag("==== testGet ===="); longinRecord *i1 = (longinRecord *)testdbRecordPtr("src:i1"); testqsrvWaitForLinkConnected(&i1->inp); testdbGetFieldEqual("target:i.VAL", DBF_LONG, 42L); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 0L); // value before first process testdbGetFieldEqual("src:i1.INP", DBF_STRING, "{\"pva\":\"target:i\"}"); testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 42L); testdbPutFieldOk("src:i1.INP", DBF_STRING, "{\"pva\":\"target:ai\"}"); testqsrvWaitForLinkConnected(&i1->inp); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 42L); // changing link doesn't automatically process testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 4L); // now it's changed } void testFieldLinks() { longinRecord *i1 = (longinRecord *)testdbRecordPtr("src:i1"); testDiag("==== Test field links ===="); std::string pv_name = "{\"pva\":{\"pv\":\"target:ai\",\"field\":\"display.precision\"}}"; testdbPutArrFieldOk("src:i1.INP$", DBF_CHAR, pv_name.length()+1, pv_name.c_str()); testqsrvWaitForLinkConnected(&i1->inp); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 4L); // changing link doesn't automatically process testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 2L); // changing link doesn't automatically process } void testProc() { longinRecord *i1 = (longinRecord *)testdbRecordPtr("src:i1"); testDiag("==== Test proc settings ===="); testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 2L); // Set it to CPP std::string pv_name = "{\"pva\":{\"pv\":\"target:ai\",\"proc\":\"CPP\"}}"; { TestMonitor m("src:i1", DBE_VALUE); testdbPutArrFieldOk("src:i1.INP$", DBF_CHAR, pv_name.length()+1, pv_name.c_str()); // wait for initial scan m.wait(); } // Link should read current value of target. testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 4L); { QSrvWaitForLinkUpdate C(&i1->inp); testdbPutFieldOk("target:ai", DBF_FLOAT, 5.0); } // now it's changed testdbGetFieldEqual("src:i1.VAL", DBF_LONG, 5L); } void testSevr() { longinRecord *i1 = (longinRecord *)testdbRecordPtr("src:i1"); testDiag("==== Test severity forwarding (NMS, MS, MSI) ===="); std::string pv_name = "{\"pva\":{\"pv\":\"target:ai\",\"sevr\":\"NMS\"}}"; testdbPutArrFieldOk("src:i1.INP$", DBF_CHAR, pv_name.length() + 1, pv_name.c_str()); testqsrvWaitForLinkConnected(&i1->inp); testdbPutFieldOk("target:ai.LOLO", DBF_FLOAT, 5.0); testdbPutFieldOk("target:ai.LLSV", DBF_STRING, "MAJOR"); testdbPutFieldOk("target:ai", DBF_FLOAT, 0.0); testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.SEVR", DBF_SHORT, epicsSevNone); pv_name = "{\"pva\":{\"pv\":\"target:ai\",\"sevr\":\"MS\"}}"; testdbPutArrFieldOk("src:i1.INP$", DBF_CHAR, pv_name.length() + 1, pv_name.c_str()); testqsrvWaitForLinkConnected(&i1->inp); testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.SEVR", DBF_SHORT, epicsSevMajor); pv_name = "{\"pva\":{\"pv\":\"target:mbbi\",\"sevr\":\"MSI\"}}"; testdbPutArrFieldOk("src:i1.INP$", DBF_CHAR, pv_name.length() + 1, pv_name.c_str()); testqsrvWaitForLinkConnected(&i1->inp); testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.SEVR", DBF_SHORT, epicsSevNone); { QSrvWaitForLinkUpdate C(&i1->inp); testdbPutFieldOk("target:ai", DBF_FLOAT, 1.0); } testdbPutFieldOk("src:i1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("src:i1.SEVR", DBF_SHORT, epicsSevInvalid); } void testPut() { testDiag("==== testPut ===="); longoutRecord *o2 = (longoutRecord *)testdbRecordPtr("src:o2"); testqsrvWaitForLinkConnected(&o2->out); testdbGetFieldEqual("target:i2.VAL", DBF_LONG, 43L); testdbGetFieldEqual("src:o2.VAL", DBF_LONG, 0L); testdbGetFieldEqual("src:o2.OUT", DBF_STRING, "{\"pva\":\"target:i2\"}"); { QSrvWaitForLinkUpdate C(&o2->out); testdbPutFieldOk("src:o2.VAL", DBF_LONG, 14L); } testdbGetFieldEqual("target:i2.VAL", DBF_LONG, 14L); testdbGetFieldEqual("src:o2.VAL", DBF_LONG, 14L); } void testStrings() { testDiag("==== testStrings ===="); stringoutRecord *so = (stringoutRecord *)testdbRecordPtr("src:str"); testdbGetFieldEqual("target:str1", DBF_STRING, "foo"); testdbPutFieldOk("target:str1.PROC", DBF_LONG, 1L); testdbGetFieldEqual("target:str1", DBF_STRING, "bar"); testdbPutFieldOk("src:str.OUT", DBF_STRING, R"({"pva" : "target:str2"})"); testqsrvWaitForLinkConnected(&so->out); { QSrvWaitForLinkUpdate C(&so->out); testdbPutFieldOk("src:str.PROC", DBF_LONG, 1L); } testdbGetFieldEqual("target:str2", DBF_STRING, "bar"); } void testToFromString() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("testToFromString:src.OUT"); testqsrvWaitForLinkConnected("testToFromString:str2.INP"); testqsrvWaitForLinkConnected("testToFromString:out.INP"); { QSrvWaitForLinkUpdate C("testToFromString:out.INP"); testdbPutFieldOk("testToFromString:src", DBR_LONG, 43); } testdbGetFieldEqual("testToFromString:str1", DBR_STRING, "43"); testdbGetFieldEqual("testToFromString:str2", DBR_STRING, "43"); testdbGetFieldEqual("testToFromString:out", DBR_LONG, 43); } void testArrays() { auto aai_inp = (aaiRecord *)testdbRecordPtr("target:aai_inp"); testDiag("==== testArrays ===="); static const epicsFloat32 input_arr[] = {1, 2, -1, 1.2, 0}; { QSrvWaitForLinkUpdate C(&aai_inp->inp); testdbPutArrFieldOk("source:aao", DBR_FLOAT, 5, input_arr); } // underlying channel cache updated, but record has not be re-processed testdbGetArrFieldEqual("target:aai_inp", DBF_CHAR, 10, 0, NULL); static const epicsInt8 expected_char[] = {1, 2, -1, 1, 0}; testdbPutFieldOk("target:aai_inp.PROC", DBF_LONG, 1L); testdbGetArrFieldEqual("target:aai_inp", DBF_CHAR, 10, 5, expected_char); static const epicsUInt32 expected_ulong[] = {1L, 2L, 4294967295L, 1L, 0}; testdbGetArrFieldEqual("target:aai_inp", DBF_ULONG, 10, 5, expected_ulong); testqsrvWaitForLinkConnected("target:aai_inp_first.INP"); testdbPutFieldOk("target:aai_inp_first.PROC", DBF_LONG, 1L); testdbGetFieldEqual("target:aai_inp_first", DBR_DOUBLE, 1.0); } void testStringArray() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("sarr:inp.INP"); const char expect[3][MAX_STRING_SIZE] = {"one", "two", "three"}; { QSrvWaitForLinkUpdate U("sarr:inp.INP"); testdbPutArrFieldOk("sarr:src", DBR_STRING, 3, expect); } testdbPutFieldOk("sarr:inp.PROC", DBR_LONG, 0); testdbGetArrFieldEqual("sarr:inp", DBR_STRING, 4, 3, expect); } void testPutAsync() { testDiag("==== testPutAsync ===="); auto trig = (longoutRecord *)testdbRecordPtr("async:trig"); auto seq = (calcRecord *)testdbRecordPtr("async:seq"); testqsrvWaitForLinkConnected(&trig->out); TestMonitor done("async:seq", DBE_VALUE, 0); testdbPutFieldOk("async:trig.PROC", DBF_LONG, 1); dbScanLock((dbCommon*)seq); while(seq->val < 2) { dbScanUnlock((dbCommon*)seq); done.wait(); dbScanLock((dbCommon*)seq); } dbScanUnlock((dbCommon*)seq); testdbGetFieldEqual("async:target", DBF_LONG, 1); testdbGetFieldEqual("async:next", DBF_LONG, 2); testdbGetFieldEqual("async:seq", DBF_LONG, 2); } void testDisconnect() { testDiag("==== %s ====", __func__); auto serv(ioc::server()); testdbPutFieldFail(-1, "disconnected.PROC", DBF_LONG, 1); testdbGetFieldEqual("disconnected.SEVR", DBF_SHORT, epicsSevInvalid); auto special(server::SharedPV::buildReadonly()); special.open(nt::NTScalar{TypeCode::Int32}.create() .update("value", 43)); serv.addPV("special:pv", special); testqsrvWaitForLinkConnected("disconnected.INP"); testdbPutFieldOk("disconnected.PROC", DBF_LONG, 1); testdbGetFieldEqual("disconnected.SEVR", DBF_SHORT, epicsSevNone); serv.removePV("special:pv"); special.close(); testqsrvWaitForLinkConnected("disconnected.INP", false); testdbPutFieldFail(-1, "disconnected.PROC", DBF_LONG, 1); testdbGetFieldEqual("disconnected.SEVR", DBF_SHORT, epicsSevInvalid); testdbPutFieldOk("disconnected.INP", DBR_STRING, ""); // avoid further log messages } void testMeta() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("meta:inp.INP"); { auto src = (aiRecord*)testdbRecordPtr("meta:src"); QSrvWaitForLinkUpdate U("meta:inp.INP"); dbScanLock((dbCommon*)src); src->tse = epicsTimeEventDeviceTime; src->time.secPastEpoch = 0x12345678; src->time.nsec = 0x10203040; #ifdef dbGetTimeStampTag src->utag = 0x00010002; #endif src->val = 7; #ifdef HAS_ALARM_MESSAGE strcpy(src->amsg, "Test"); #endif dbProcess((dbCommon*)src); dbScanUnlock((dbCommon*)src); } auto inp = (aiRecord*)testdbRecordPtr("meta:inp"); long ret, nelem; epicsEnum16 stat, sevr; char egu[10] = ""; short prec; double val, lolo, low, high, hihi; dbScanLock((dbCommon*)inp); testTrue(dbIsLinkConnected(&inp->inp)!=0); testEq(dbGetLinkDBFtype(&inp->inp), DBF_DOUBLE); // alarm and time meta-data will be "latched" by a call to dbGetLink. // until then, the initial values are used testTrue((ret=dbGetAlarm(&inp->inp, &stat, &sevr))==0 && stat==LINK_ALARM && sevr==INVALID_ALARM) <<" ret="<inp, 0x12345678, 0x10203040, 0x00010002); testTrue((ret=dbGetGraphicLimits(&inp->inp, &low, &high))==0 && low==-9 && high==9) <<" ret="<inp, &nelem))==0 && nelem==1) <<" ret="<time.secPastEpoch = 0x12345678; prec->time.nsec = 0x10203041; return 0; } void testMetaMS() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("meta:inp:ms.INP"); { QSrvWaitForLinkUpdate U("meta:inp:ms.INP"); testdbPutFieldOk("meta:src:ms.PROC", DBR_LONG, 0); } auto inp = (aiRecord*)testdbRecordPtr("meta:inp:ms"); dbScanLock((dbCommon*)inp); testTrue(dbIsLinkConnected(&inp->inp)!=0); testEq(dbGetLinkDBFtype(&inp->inp), DBF_DOUBLE); testOk(inp->nsev==0 && inp->nsta==0 #ifdef HAS_ALARM_MESSAGE && inp->namsg[0]==0 #endif , "NSEVR=%u NSTAT=%u NAMSG=\"%s\"", inp->nsev, inp->nsta, #ifdef HAS_ALARM_MESSAGE inp->namsg #else "N/A" #endif ); long ret; double val; testTrue((ret=dbGetLink(&inp->inp, DBR_DOUBLE, &val, nullptr, nullptr))==0 && val==0.0)<<" ret="<nsev, inp->nsta, #ifdef HAS_ALARM_MESSAGE inp->namsg #else "N/A" #endif ); dbScanUnlock((dbCommon*)inp); } void testFwd() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("flnk:src.FLNK"); testdbGetFieldEqual("flnk:tgt", DBF_LONG, 0); testdbPutFieldOk("flnk:src.PROC", DBF_LONG, 1); { auto prec = testdbRecordPtr("flnk:tgt"); TestMonitor mon("flnk:tgt", DBE_VALUE); dbScanLock(prec); while(((calcRecord*)prec)->val==0) { dbScanUnlock(prec); mon.wait(); dbScanLock(prec); } dbScanUnlock(prec); } testdbGetFieldEqual("flnk:tgt", DBF_LONG, 1); } void testAtomic() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("atomic:lnk:1.INP"); testqsrvWaitForLinkConnected("atomic:lnk:2.INP"); { QSrvWaitForLinkUpdate A("atomic:lnk:1.INP"); QSrvWaitForLinkUpdate B("atomic:lnk:2.INP"); testdbPutFieldOk("atomic:src:1.PROC", DBR_LONG, 0); } epicsUInt32 expect; { auto src1(testdbRecordPtr("atomic:src:1")); dbScanLock(src1); expect = ((calcoutRecord*)src1)->val; testEq(expect & ~0xff, 0u); expect |= expect<<8u; dbScanUnlock(src1); } testdbGetFieldEqual("atomic:lnk:out", DBF_ULONG, expect); } void testEnum() { testDiag("==== %s ====", __func__); testqsrvWaitForLinkConnected("enum:src:b.OUT"); testqsrvWaitForLinkConnected("enum:src:s.OUT"); testqsrvWaitForLinkConnected("enum:tgt:s.INP"); testqsrvWaitForLinkConnected("enum:tgt:b.INP"); { QSrvWaitForLinkUpdate A("enum:tgt:b.INP"); // last in chain... testdbPutFieldOk("enum:src:b", DBR_STRING, "one"); } testdbGetFieldEqual("enum:tgt:s", DBR_STRING, "one"); // not clear how to handle this case, where a string is // read as DBR_USHORT, which is actually as DBF_ENUM testTodoBegin("Not yet implemented"); testdbGetFieldEqual("enum:tgt:b", DBR_STRING, "one"); testTodoEnd(); } void testNTNDArray() { testDiag("==== %s ====", __func__); auto serv(ioc::server()); auto ntndarray(server::SharedPV::buildReadonly()); auto top = nt::NTNDArray{}.create(); top["value->floatValue"] = shared_array({0.0}); ntndarray.open(top); serv.addPV("source:ntndarray", ntndarray); testqsrvWaitForLinkConnected("target:ntndarray_inp.INP"); { QSrvWaitForLinkUpdate A("target:ntndarray_inp.INP"); auto update = ntndarray.fetch().cloneEmpty(); update["value->floatValue"] = shared_array({0.1, 0.2, 0.3, 0.4, 0.5}); ntndarray.post(update); } static const epicsFloat32 expected_float_A[] = {0.1, 0.2, 0.3, 0.4, 0.5}; testdbGetArrFieldEqual("target:ntndarray_inp", DBF_FLOAT, 5, 5, expected_float_A); { QSrvWaitForLinkUpdate B("target:ntndarray_inp.INP"); auto update = ntndarray.fetch().cloneEmpty(); update["value->floatValue"] = shared_array({0.6, 0.7, 0.8, 0.9, 0.91}); ntndarray.post(update); } static const epicsFloat32 expected_float_B[] = {0.6, 0.7, 0.8, 0.9, 0.91}; testdbGetArrFieldEqual("target:ntndarray_inp", DBF_FLOAT, 5, 5, expected_float_B); serv.removePV("source:ntndarray"); ntndarray.close(); } void testiocsh() { testDiag("==== %s ====", __func__); { CaptureStd cap([](){ iocshCmd("dbpvar \"\" 5"); }); testStrEq(cap.err(), ""); testStrMatch(".*PVA links in all records.*async:target conn=T.*", cap.out()); } { CaptureStd cap([](){ iocshCmd("dbjlr \"\" 5"); }); testStrEq(cap.err(), ""); testStrMatch(".*\'pva\': testToFromString:str1.*", cap.out()); } } } // namespace extern "C" void testioc_registerRecordDeviceDriver(struct dbBase *); MAIN(testpvalink) { testPlan(106); testSetup(); pvxs::logger_config_env(); try { TestIOC IOC; testdbReadDatabase("testioc.dbd", NULL, NULL); testioc_registerRecordDeviceDriver(pdbbase); registryFunctionAdd("setAMSG", (REGISTRYFUNCTION)&setAMSG); testdbReadDatabase("testpvalink.db", NULL, NULL); IOC.init(); testGet(); testFieldLinks(); testProc(); testSevr(); testPut(); testStrings(); testToFromString(); testArrays(); testStringArray(); testPutAsync(); testDisconnect(); testMeta(); testMetaMS(); testFwd(); testAtomic(); testEnum(); testNTNDArray(); testiocsh(); } catch (std::exception &e) { testFail("Unexpected exception: %s", e.what()); } // call epics atexits explicitly to handle older base w/o de-init hooks epicsExitCallAtExits(); cleanup_for_valgrind(); return testDone(); }