diff --git a/.ci b/.ci new file mode 160000 index 0000000..dead44c --- /dev/null +++ b/.ci @@ -0,0 +1 @@ +Subproject commit dead44c3cbe70b134766af13a56b571dbb40a278 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 0000000..7346fd7 --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,74 @@ +name: caPutLog CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +env: + SETUP_PATH: .ci + +jobs: + build: + name: ${{ matrix.os }}/${{ matrix.cmp }}/${{ matrix.configuration }}/${{ matrix.cross }} + runs-on: ${{ matrix.os}} + env: + CMP: ${{ matrix.cmp }} + BCFG: ${{ matrix.configuration }} + CI_CROSS_TARGETS: ${{ matrix.cross }} + TEST: ${{ matrix.test }} + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + cmp: gcc + configuration: default + + - os: ubuntu-latest + cmp: gcc + configuration: static + + - os: windows-2019 + cmp: vs2019 + configuration: static + + - os: ubuntu-latest + cmp: gcc + configuration: default + cross: "RTEMS-pc386-qemu@4.10" + test: NO + + - os: macos-12 + cmp: clang + configuration: default + + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Prepare and compile EPICS dependencies + run: python .ci/cue.py prepare + + - name: Build main module + run: python .ci/cue.py build + + - name: Run main module tests + run: python .ci/cue.py -T 20M test + + - name: Upload tapfiles Artifact + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: tapfiles ${{ matrix.name }} + path: "**/O.*/*.tap" + if-no-files-found: ignore + + - name: Collect and show test results + if: ${{ always() }} + run: python .ci/cue.py -T 5M test-results diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cfa3a70 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".ci"] + path = .ci + url = https://github.com/epics-base/ci-scripts diff --git a/caPutLogApp/caPutJsonLogShellCommands.cpp b/caPutLogApp/caPutJsonLogShellCommands.cpp index 047d3b1..aa7dd7f 100644 --- a/caPutLogApp/caPutJsonLogShellCommands.cpp +++ b/caPutLogApp/caPutJsonLogShellCommands.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "caPutJsonLogTask.h" @@ -97,6 +98,27 @@ extern "C" #endif } + /* Metadata */ + int caPutJsonLogAddMetadata(char *property, char *value){ + CaPutJsonLogTask *logger = CaPutJsonLogTask::getInstance(); + std::string property_str(property); + std::string value_str(value); + if (logger != NULL) return logger->addMetadata(property_str, value_str); + else return -1; + } + static const iocshArg caPutJsonLogAddMetadataArg0 = {"property", iocshArgString}; + static const iocshArg caPutJsonLogAddMetadataArg1 = {"value", iocshArgString}; + static const iocshArg *const caPutJsonLogAddMetadataArgs[] = + { + &caPutJsonLogAddMetadataArg0, + &caPutJsonLogAddMetadataArg1 + }; + static const iocshFuncDef caPutJsonLogAddMetadataDef = {"caPutJsonLogAddMetadata", 2, caPutJsonLogAddMetadataArgs}; + static void caPutJsonLogAddMetadataCall(const iocshArgBuf *args) + { + caPutJsonLogAddMetadata(args[0].sval, args[1].sval); + } + /* Register JSON IOCsh commands */ static void caPutJsonLogRegister(void) { @@ -108,6 +130,7 @@ extern "C" iocshRegister(&caPutJsonLogReconfDef,caPutJsonLogReconfCall); iocshRegister(&caPutJsonLogShowDef,caPutJsonLogShowCall); iocshRegister(&caPutLogInitDef,caPutLogInitCall); + iocshRegister(&caPutJsonLogAddMetadataDef,caPutJsonLogAddMetadataCall); caPutLogRegisterDone = 2; break; diff --git a/caPutLogApp/caPutJsonLogTask.cpp b/caPutLogApp/caPutJsonLogTask.cpp index da15ac2..33f3407 100644 --- a/caPutLogApp/caPutJsonLogTask.cpp +++ b/caPutLogApp/caPutJsonLogTask.cpp @@ -13,8 +13,10 @@ // Standard library imports #include +#include #include #include +#include #include #ifdef _WIN32 @@ -116,6 +118,42 @@ caPutJsonLogStatus CaPutJsonLogTask::report(int level) } } +caPutJsonLogStatus CaPutJsonLogTask::addMetadata(std::string property, std::string value) +{ + std::pair::iterator, bool> ret; + ret = metadata.insert(std::pair(property,value)); + if ( ret.second == false ) { + metadata.erase(property); + ret = metadata.insert(std::pair(property,value)); + if (ret.second == false) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: fail to add property %s to json log\n", property.c_str()); + return caPutJsonLogError; + } + } + errlogSevPrintf(errlogInfo, "caPutJsonLog: add property %s with value %s to json log\n", property.c_str(), value.c_str()); + return caPutJsonLogSuccess; +} + +bool CaPutJsonLogTask::isMetadataKey(std::string property) +{ + return metadata.count(property) > 0; +} + +void CaPutJsonLogTask::removeAllMetadata() +{ + metadata.clear(); +} + +size_t CaPutJsonLogTask::metadataCount() +{ + return metadata.size(); +} + +std::map CaPutJsonLogTask::getMetadata() +{ + return metadata; +} + caPutJsonLogStatus CaPutJsonLogTask::initialize(const char* addresslist, caPutJsonLogConfig config) { caPutJsonLogStatus status; @@ -447,6 +485,17 @@ caPutJsonLogStatus CaPutJsonLogTask::buildJsonMsg(const VALUE *pold_value, const reinterpret_cast(pLogData->userid), strlen(pLogData->userid))); + // Add metadata + std::map::iterator meta_it; + for(meta_it = metadata.begin(); meta_it != metadata.end(); meta_it++){ + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, + reinterpret_cast(meta_it->first.c_str()), + meta_it->first.length())); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, + reinterpret_cast(meta_it->second.c_str()), + meta_it->second.length())); + } + // Add PV name const unsigned char str_pvName[] = "pv"; CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_pvName, diff --git a/caPutLogApp/caPutJsonLogTask.h b/caPutLogApp/caPutJsonLogTask.h index 67a7832..92cd89b 100644 --- a/caPutLogApp/caPutJsonLogTask.h +++ b/caPutLogApp/caPutJsonLogTask.h @@ -21,6 +21,7 @@ #include #include #include +#include #include // Includes from this module @@ -142,6 +143,41 @@ class epicsShareClass CaPutJsonLogTask { */ caPutJsonLogStatus report(int level); + /** + * @brief Add IOC metadata to json output + * + * @param property JSON property + * @param value Value associated with property + */ + caPutJsonLogStatus addMetadata(std::string property, std::string value); + + /** + * @brief Check if a property is a key in the metadata map + * + * @param property JSON property + * @return bool Status. + */ + bool isMetadataKey(std::string property); + + /** + * @brief Clears the metadata map + */ + void removeAllMetadata(); + + /** + * @brief Gets the number of elements in the metadata map + * + * @return size_t Number of elements in metadata + */ + size_t metadataCount(); + + /** + * @brief Gets the metadata list object + * + * @return map the Metadata map + */ + std::map getMetadata(); + private: // Singleton instance of this class. @@ -172,6 +208,9 @@ class epicsShareClass CaPutJsonLogTask { // Total count of logged puts int caPutTotalCount; // To modify or read this value only epicsAtomic methods should be used + // IOC metadata + std::map metadata; + // Class methods (Do not allow public constructors - class is designed as singleton) CaPutJsonLogTask(); virtual ~CaPutJsonLogTask(); diff --git a/test/Makefile b/test/Makefile index c1427df..8f1e420 100644 --- a/test/Makefile +++ b/test/Makefile @@ -8,6 +8,7 @@ include $(TOP)/configure/CONFIG DBDDEPENDS_FILES += dbTestIoc.dbd$(DEP) TARGETS += $(COMMON_DIR)/dbTestIoc.dbd dbTestIoc_DBD += base.dbd +CXXFLAGS=-std=c++11 # Libraries to which the test executable is linked PROD_LIBS = caPutLog diff --git a/test/caPutJsonLogTest.cpp b/test/caPutJsonLogTest.cpp index b6411d2..a88e2de 100644 --- a/test/caPutJsonLogTest.cpp +++ b/test/caPutJsonLogTest.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,7 @@ static epicsEvent testLogServerMsgReady; static dbEventCtx testEvtCtx; static std::string logServerAddress; static int stopLogServer = 0; +CaPutJsonLogTask *logger; @@ -97,7 +99,7 @@ static void readFromClient(void *pParam) memset(recvbuf, 0, BUFFER_SIZE); recvLength = recv(insock, recvbuf, BUFFER_SIZE, 0); if (recvLength > 0) { - incLogMsg.append(recvbuf); + incLogMsg.append(recvbuf, recvLength); if (incLogMsg.find('\n') != std::string::npos) { testLogServerMsgReady.trigger(); } @@ -217,6 +219,7 @@ class JsonParser { std::vector newVal; std::vector oldVal; + std::map metadata; int newSize; int oldSize; @@ -383,6 +386,18 @@ class JsonParser { jsonParser->waitingKey = true; } } + else if (logger->isMetadataKey(jsonParser->currentKey)) { + std::pair::iterator, bool> ret; + ret = jsonParser->metadata.insert(std::pair(jsonParser->currentKey, + std::string(reinterpret_cast(stringVal), stringLen))); + if (ret.second == false) { + testAbort("caPutJsonLog: fail to add property %s to json log\n", jsonParser->currentKey.c_str()); + return caPutJsonLogError; + } + if (!jsonParser->inArray) { + jsonParser->waitingKey = true; + } + } else { testAbort("JsonParser: Unexpected string callback in Json"); return 0; @@ -427,13 +442,19 @@ class JsonParser { jsonParser->currentKey.assign(reinterpret_cast(key), stringLen); jsonParser->waitingKey = false; return 1; - } + } }; /******************************************************************************* * Tests helpers *******************************************************************************/ +bool metadata_compare (std::map metadataA, std::map metadataB) { + bool testSize = metadataA.size() == metadataB.size(); + bool testContent = std::equal(metadataA.begin(), metadataA.end(), metadataB.begin()); + return testSize && testContent; + } + void commonTests(JsonParser &jsonParser, const char* pvname, const char * testPrefix) { // Test if is Json terminated with newline @@ -460,7 +481,9 @@ void commonTests(JsonParser &jsonParser, const char* pvname, const char * testPr // Test hostname char hostname[1024]; gethostname(hostname, 1024); - testOk(!jsonParser.host.compare(hostname), + + // asLibRoutines changes the hostnames to lower-case; some OSs are not so kind. + testOk(!epicsStrCaseCmp(hostname, jsonParser.host.c_str()), "%s - %s", testPrefix, "Hostname check"); // Test username @@ -471,6 +494,11 @@ void commonTests(JsonParser &jsonParser, const char* pvname, const char * testPr testOk(!jsonParser.pv.compare(pvname), "%s - %s", testPrefix, "PV name check"); + + //Test Metadata + testOk(metadata_compare(jsonParser.metadata, logger->getMetadata()), + "%s - %s - Parser(%lu) vs Logger(%lu)", testPrefix, "Metadata keys and values check", + logger->metadataCount(), jsonParser.metadata.size()); } template @@ -692,6 +720,20 @@ void testDbf(const char *pv, chtype type, } } +void testMetadataHelper(std::map metadata ) +{ + char desc1[] = "Description 1"; + char desc2[] = "Description 2"; + std::map::iterator meta_it; + for(meta_it = metadata.begin(); meta_it != metadata.end(); meta_it++){ + logger->addMetadata(meta_it->first,meta_it->second); + } + testDbf("longout_DBF_STRING.DESC", DBR_STRING, desc1, 1, desc2, 1, "Metadata test"); + testOk(metadata_compare(metadata, logger->getMetadata()), + "Metadata test - Metadata keys and values check - Original test(%lu) vs Logger(%lu)", + metadata.size(), logger->metadataCount()); + logger->removeAllMetadata(); +} /******************************************************************************* * Tests @@ -812,6 +854,39 @@ void runTests(void *arg) { &charArray1[0], strlen(charArray1), &charArray2[0], strlen(charArray1), "DBF_CHAR array (lso) test"); + // Test metadata + std::map metadata_test_single = { + {"ABCDEFGHIJKLMNOPQRS", "1234567890_1234567890"} + }; + std::map metadata_test_escape_chars = { + {"ABC\n##&¤67  space & \'\\n#", "CBC\n##&¤67  space & \'\\n#"} + }; + std::map metadata_test_multiple = { + {"One", "1"}, {"Two", "2"}, {"Three", "3"}, {"Four", "4"}, {"Five", "5"}, + {"Six", "6"}, {"Seven", "7"}, {"Eight", "8"}, {"Nine", "9"}, {"Ten", "10"}, + }; + + std::map metadata_test_reinsert_new_val = { + {"Five", "5"}, {"Six", "6"}, + }; + + testMetadataHelper(metadata_test_single); + testMetadataHelper(metadata_test_escape_chars); + testMetadataHelper(metadata_test_multiple); + + //Reinsert metadata test + logger->addMetadata("Five", "Five"); + logger->addMetadata("Six", "Six"); + testMetadataHelper(metadata_test_reinsert_new_val); + + // 1000+ elements in metadata + std::map metadata_test_big; + for(int i = 0; i <=100; i++) + { + metadata_test_big.insert(std::pair(std::to_string(i), std::to_string(i))); + } + testMetadataHelper(metadata_test_big); + //Destroy test thread CA context ca_context_destroy(); @@ -844,7 +919,7 @@ void stopIoc(){ MAIN(caPutJsonLogTests) { - testPlan(375); + testPlan(499); // Create thread for log server const char * logServerThreadName = "testLogServer"; @@ -879,7 +954,7 @@ MAIN(caPutJsonLogTests) iocLogPrefix(logMsgPrefix); asSetFilename("../asg.cfg"); startIoc(); - CaPutJsonLogTask *logger = CaPutJsonLogTask::getInstance(); + logger = CaPutJsonLogTask::getInstance(); if (logger == NULL) testAbort("Failed to initialize logger."); logger->initialize(logServerAddress.c_str(), caPutJsonLogOnChange); testDiag("Test IOC ready");