/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) */ #include #include #include #include #include #include #include #include #include #include #include "server/ua_server_internal.h" #include #include #include #include "test_helpers.h" #include "testing_clock.h" #include "thread_wrapper.h" #include "historical_read_test_data.h" #include "randomindextest_backend.h" static UA_Server *server; static UA_HistoryDataGathering *gathering; static UA_atomic(uintptr_t) running; static THREAD_HANDLE server_thread; static UA_Client *client; static UA_NodeId parentNodeId; static UA_NodeId parentReferenceNodeId; static UA_NodeId outNodeId; static UA_DateTime *testDataSorted; #ifdef UA_ENABLE_MALLOC_SINGLETON static UA_Boolean failHistoryDataAllocation; static UA_Boolean failHistoryUpdateResultsAllocation; static UA_Boolean historyReadBackendCalled; static void * historyDataTestCalloc(size_t nelem, size_t elsize) { if(failHistoryDataAllocation && nelem == 1 && elsize == UA_TYPES[UA_TYPES_HISTORYDATA].memSize) { failHistoryDataAllocation = false; return NULL; } if(failHistoryUpdateResultsAllocation && nelem == 1 && elsize == UA_TYPES[UA_TYPES_STATUSCODE].memSize) { failHistoryUpdateResultsAllocation = false; return NULL; } return calloc(nelem, elsize); } static void historyReadTestBackend(UA_Server *server, void *hdbContext, const UA_NodeId *sessionId, void *sessionContext, const UA_RequestHeader *requestHeader, const UA_ReadRawModifiedDetails *historyReadDetails, UA_TimestampsToReturn timestampsToReturn, UA_Boolean releaseContinuationPoints, size_t nodesToReadSize, const UA_HistoryReadValueId *nodesToRead, UA_HistoryReadResponse *response, UA_HistoryData * const * const historyData) { historyReadBackendCalled = true; } #endif THREAD_CALLBACK(serverloop) { while(UA_atomic_load(&running)) { UA_Server_run_iterate(server, false); } return 0; } static void setup(void) { UA_atomic_store(&running, true); server = UA_Server_newForUnitTest(); ck_assert(server != NULL); UA_ServerConfig *config = UA_Server_getConfig(server); gathering = (UA_HistoryDataGathering*)UA_calloc(1, sizeof(UA_HistoryDataGathering)); *gathering = UA_HistoryDataGathering_Default(1); config->historyDatabase = UA_HistoryDatabase_default(*gathering); UA_StatusCode retval = UA_Server_run_startup(server); if(retval != UA_STATUSCODE_GOOD) { fprintf(stderr, "Error while calling Server_run_startup. %s\n", UA_StatusCode_name(retval)); UA_Server_delete(server); exit(1); } THREAD_CREATE(server_thread, serverloop); /* Define the attribute of the uint32 variable node */ UA_VariableAttributes attr = UA_VariableAttributes_default; UA_UInt32 myUint32 = 40; UA_Variant_setScalar(&attr.value, &myUint32, &UA_TYPES[UA_TYPES_UINT32]); attr.description = UA_LOCALIZEDTEXT("en-US","the answer"); attr.displayName = UA_LOCALIZEDTEXT("en-US","the answer"); attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId; attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD | UA_ACCESSLEVELMASK_HISTORYWRITE | UA_ACCESSLEVELMASK_STATUSWRITE | UA_ACCESSLEVELMASK_TIMESTAMPWRITE; attr.historizing = true; /* Add the variable node to the information model */ UA_NodeId uint32NodeId = UA_NODEID_STRING(1, "the.answer"); UA_QualifiedName uint32Name = UA_QUALIFIEDNAME(1, "the answer"); parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); UA_NodeId_init(&outNodeId); retval = UA_Server_addVariableNode(server, uint32NodeId, parentNodeId, parentReferenceNodeId, uint32Name, UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, &outNodeId); if (retval != UA_STATUSCODE_GOOD) { fprintf(stderr, "Error adding variable node. %s\n", UA_StatusCode_name(retval)); UA_Server_delete(server); exit(1); } client = UA_Client_newForUnitTest(); retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); if (retval != UA_STATUSCODE_GOOD) { fprintf(stderr, "Client can not connect to opc.tcp://localhost:4840. %s\n", UA_StatusCode_name(retval)); UA_Client_delete(client); UA_Server_delete(server); exit(1); } } static void teardown(void) { /* cleanup */ UA_Client_disconnect(client); UA_Client_delete(client); UA_atomic_store(&running, false); THREAD_JOIN(server_thread); UA_NodeId_clear(&parentNodeId); UA_NodeId_clear(&parentReferenceNodeId); UA_NodeId_clear(&outNodeId); UA_Server_run_shutdown(server); UA_Server_delete(server); UA_free(gathering); } static UA_StatusCode setUInt32(UA_Client *thisClient, UA_NodeId node, UA_UInt32 value) { UA_EventLoop *el = UA_Client_getConfig(thisClient)->eventLoop; UA_DataValue dv; UA_DataValue_init(&dv); UA_Variant_setScalar(&dv.value, &value, &UA_TYPES[UA_TYPES_UINT32]); dv.hasValue = true; dv.sourceTimestamp = el->dateTime_now(el); dv.hasSourceTimestamp = true; return UA_Client_writeValueAttributeEx(thisClient, node, &dv); } static UA_DateTime * sortDateTimes(UA_DateTime *data) { size_t count = 0; while(data[count++]); UA_DateTime* ret; if (UA_Array_copy(data, count, (void**)&ret, &UA_TYPES[UA_TYPES_DATETIME]) != UA_STATUSCODE_GOOD) return NULL; --count; // sort it for (size_t i = 1; i < count; i++) { for (size_t j = 0; j < count - i; j++) { if (ret[j] > ret[j+1]) { UA_DateTime tmp = ret[j]; ret[j] = ret[j+1]; ret[j+1] = tmp; } } } return ret; } static void printTimestamp(UA_DateTime timestamp) { if (timestamp == TIMESTAMP_FIRST) { fprintf(stderr, "FIRST,"); } else if (timestamp == TIMESTAMP_LAST) { fprintf(stderr, "LAST,"); } else { fprintf(stderr, "%3lld,", timestamp / UA_DATETIME_SEC); } } static void printResult(UA_DataValue * value) { if (value->status != UA_STATUSCODE_GOOD) fprintf(stderr, "%s:", UA_StatusCode_name(value->status)); printTimestamp(value->sourceTimestamp); } static UA_Boolean resultIsEqual(const UA_DataValue * result, const testTuple * tuple, size_t index) { switch (tuple->result[index]) { case TIMESTAMP_FIRST: if (result->status != UA_STATUSCODE_BADBOUNDNOTFOUND || !UA_Variant_isEmpty(&result->value)) return false; /* we do not test timestamp if TIMESTAMP_UNSPECIFIED is given for start. * See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark b for details.*/ if (tuple->start != TIMESTAMP_UNSPECIFIED && tuple->start != result->sourceTimestamp) return false; break; case TIMESTAMP_LAST: if (result->status != UA_STATUSCODE_BADBOUNDNOTFOUND || !UA_Variant_isEmpty(&result->value)) return false; /* we do not test timestamp if TIMESTAMP_UNSPECIFIED is given for end. * See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark a for details.*/ if (tuple->end != TIMESTAMP_UNSPECIFIED && tuple->end != result->sourceTimestamp) return false; break; default: if (result->sourceTimestamp != tuple->result[index] || result->value.type != &UA_TYPES[UA_TYPES_INT64] || *((UA_Int64*)result->value.data) != tuple->result[index]) return false; } return true; } static UA_Boolean fillHistoricalDataBackend(UA_HistoryDataBackend backend) { int i = 0; UA_DateTime currentDateTime = testData[i]; fprintf(stderr, "Adding to historical data backend: "); while (currentDateTime) { fprintf(stderr, "%lld, ", currentDateTime / UA_DATETIME_SEC); UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Int64 d = currentDateTime; UA_Variant_setScalarCopy(&value.value, &d, &UA_TYPES[UA_TYPES_INT64]); value.hasSourceTimestamp = true; value.sourceTimestamp = currentDateTime; value.hasServerTimestamp = true; value.serverTimestamp = currentDateTime; value.hasStatus = true; value.status = UA_STATUSCODE_GOOD; if (backend.serverSetHistoryData(server, backend.context, NULL, NULL, &outNodeId, UA_FALSE, &value) != UA_STATUSCODE_GOOD) { fprintf(stderr, "\n"); return false; } UA_DataValue_clear(&value); currentDateTime = testData[++i]; } fprintf(stderr, "\n"); return true; } static void requestHistoryRange(UA_DateTime start, UA_DateTime end, UA_HistoryReadResponse * response, UA_UInt32 numValuesPerNode, UA_Boolean returnBounds, UA_ByteString *continuationPoint, const char *indexRange) { UA_ReadRawModifiedDetails *details = UA_ReadRawModifiedDetails_new(); details->startTime = start; details->endTime = end; details->isReadModified = false; details->numValuesPerNode = numValuesPerNode; details->returnBounds = returnBounds; UA_HistoryReadValueId *valueId = UA_HistoryReadValueId_new(); UA_NodeId_copy(&outNodeId, &valueId->nodeId); if (continuationPoint) UA_ByteString_copy(continuationPoint, &valueId->continuationPoint); if (indexRange) valueId->indexRange = UA_STRING_ALLOC(indexRange); UA_HistoryReadRequest request; UA_HistoryReadRequest_init(&request); request.historyReadDetails.encoding = UA_EXTENSIONOBJECT_DECODED; request.historyReadDetails.content.decoded.type = &UA_TYPES[UA_TYPES_READRAWMODIFIEDDETAILS]; request.historyReadDetails.content.decoded.data = details; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; request.nodesToReadSize = 1; request.nodesToRead = valueId; lockServer(server); Service_HistoryRead(server, &server->adminSession, &request, response); unlockServer(server); UA_HistoryReadRequest_clear(&request); } static void requestHistory(UA_DateTime start, UA_DateTime end, UA_HistoryReadResponse * response, UA_UInt32 numValuesPerNode, UA_Boolean returnBounds, UA_ByteString *continuationPoint) { requestHistoryRange(start, end, response, numValuesPerNode, returnBounds, continuationPoint, NULL); } static UA_UInt32 testHistoricalDataBackend(size_t maxResponseSize) { const UA_HistorizingNodeIdSettings *setting = gathering->getHistorizingSetting(server, gathering->context, &outNodeId); UA_HistorizingNodeIdSettings newSetting = *setting; newSetting.maxHistoryDataResponseSize = maxResponseSize; gathering->updateNodeIdSetting(server, gathering->context, &outNodeId, newSetting); UA_UInt32 retval = 0; size_t i = 0; testTuple *current = &testRequests[i]; fprintf(stderr, "Testing with maxResponseSize of %llu\n", (long long unsigned)maxResponseSize); fprintf(stderr, "Start | End | numValuesPerNode | returnBounds |ContPoint| " "{Expected}{Result} Result\n"); fprintf(stderr, "------+------+------------------+" "--------------+---------+----------------\n"); size_t j; while(current->start || current->end) { j = 0; if(current->start == TIMESTAMP_UNSPECIFIED) { fprintf(stderr, "UNSPEC|"); } else { fprintf(stderr, " %3lld |", current->start / UA_DATETIME_SEC); } if(current->end == TIMESTAMP_UNSPECIFIED) { fprintf(stderr, "UNSPEC|"); } else { fprintf(stderr, " %3lld |", current->end / UA_DATETIME_SEC); } fprintf(stderr, " %2u | %s | %s | {", current->numValuesPerNode, (current->returnBounds ? "Yes" : " No"), (current->returnContinuationPoint ? "Yes" : " No")); while (current->result[j]) { printTimestamp(current->result[j]); ++j; } fprintf(stderr, "}"); UA_DataValue *result = NULL; size_t resultSize = 0; UA_ByteString continuous; UA_ByteString_init(&continuous); UA_Boolean readOk = true; size_t reseivedValues = 0; fprintf(stderr, "{"); size_t counter = 0; do { UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); UA_UInt32 numValuesPerNode = current->numValuesPerNode; if (numValuesPerNode > 0 && numValuesPerNode + (UA_UInt32)reseivedValues > current->numValuesPerNode) numValuesPerNode = current->numValuesPerNode - (UA_UInt32)reseivedValues; requestHistory(current->start, current->end, &response, numValuesPerNode, current->returnBounds, &continuous); ++counter; if(response.resultsSize != 1) { fprintf(stderr, "ResultError:Size %llu %s", (long long unsigned)response.resultsSize, UA_StatusCode_name(response.responseHeader.serviceResult)); readOk = false; UA_HistoryReadResponse_clear(&response); break; } UA_StatusCode stat = response.results[0].statusCode; if (stat == UA_STATUSCODE_BADBOUNDNOTSUPPORTED && current->returnBounds) { fprintf(stderr, "%s", UA_StatusCode_name(stat)); UA_HistoryReadResponse_clear(&response); break; } if(response.results[0].historyData.encoding != UA_EXTENSIONOBJECT_DECODED || response.results[0].historyData.content.decoded.type != &UA_TYPES[UA_TYPES_HISTORYDATA]) { fprintf(stderr, "ResultError:HistoryData"); readOk = false; UA_HistoryReadResponse_clear(&response); break; } UA_HistoryData * data = (UA_HistoryData *)response.results[0].historyData.content.decoded.data; resultSize = data->dataValuesSize; result = data->dataValues; if (resultSize == 0 && continuous.length > 0) { fprintf(stderr, "continuousResultEmpty"); readOk = false; UA_HistoryReadResponse_clear(&response); break; } if (resultSize > maxResponseSize) { fprintf(stderr, "resultToBig"); readOk = false; UA_HistoryReadResponse_clear(&response); break; } if (stat != UA_STATUSCODE_GOOD) { fprintf(stderr, "%s", UA_StatusCode_name(stat)); } else { for (size_t k = 0; k < resultSize; ++k) printResult(&result[k]); } if (stat == UA_STATUSCODE_GOOD && j >= resultSize + reseivedValues) { for (size_t l = 0; l < resultSize; ++l) { /* See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark a for details.*/ if (current->result[l + reseivedValues] == TIMESTAMP_LAST && current->end == TIMESTAMP_UNSPECIFIED) { // This test will work on not continuous read, only if (reseivedValues == 0 && !(l > 0 && result[l].sourceTimestamp == result[l-1].sourceTimestamp + UA_DATETIME_SEC)) readOk = false; } /* See OPC UA Part 11, Version 1.03, Page 5-6, Table 1, Mark b for details.*/ if (current->result[l + reseivedValues] == TIMESTAMP_FIRST && current->start == TIMESTAMP_UNSPECIFIED) { // This test will work on not continuous read, only if (reseivedValues == 0 && !(l > 0 && result[l].sourceTimestamp == result[l-1].sourceTimestamp - UA_DATETIME_SEC)) readOk = false; } if (!resultIsEqual(&result[l], current, l + reseivedValues)) readOk = false; } if (response.results[0].continuationPoint.length > 0) fprintf(stderr, "C,"); reseivedValues += resultSize; if (reseivedValues == j) { if (current->returnContinuationPoint && response.results[0].continuationPoint.length == 0) { readOk = false; fprintf(stderr, "missingContinuationPoint"); } if (!current->returnContinuationPoint && response.results[0].continuationPoint.length > 0) { readOk = false; fprintf(stderr, "unexpectedContinuationPoint"); } UA_HistoryReadResponse_clear(&response); break; } UA_ByteString_clear(&continuous); UA_ByteString_copy(&response.results[0].continuationPoint, &continuous); } else { readOk = false; UA_HistoryReadResponse_clear(&response); break; } UA_HistoryReadResponse_clear(&response); } while (continuous.length > 0); if (j != reseivedValues) { readOk = false; } UA_ByteString_clear(&continuous); if (!readOk) { fprintf(stderr, "} Fail (%llu requests)\n", (long long unsigned)counter); ++retval; } else { fprintf(stderr, "} OK (%llu requests)\n", (long long unsigned)counter); } current = &testRequests[++i]; } return retval; } static UA_StatusCode deleteHistory(UA_DateTime start, UA_DateTime end) { UA_DeleteRawModifiedDetails *details = UA_DeleteRawModifiedDetails_new(); details->startTime = start; details->endTime = end; details->isDeleteModified = false; UA_NodeId_copy(&outNodeId, &details->nodeId); UA_HistoryUpdateRequest request; UA_HistoryUpdateRequest_init(&request); request.historyUpdateDetailsSize = 1; request.historyUpdateDetails = UA_ExtensionObject_new(); UA_ExtensionObject_init(request.historyUpdateDetails); request.historyUpdateDetails[0].encoding = UA_EXTENSIONOBJECT_DECODED; request.historyUpdateDetails[0].content.decoded.type = &UA_TYPES[UA_TYPES_DELETERAWMODIFIEDDETAILS]; request.historyUpdateDetails[0].content.decoded.data = details; UA_HistoryUpdateResponse response; UA_HistoryUpdateResponse_init(&response); lockServer(server); Service_HistoryUpdate(server, &server->adminSession, &request, &response); unlockServer(server); UA_HistoryUpdateRequest_clear(&request); UA_StatusCode ret = UA_STATUSCODE_GOOD; if (response.responseHeader.serviceResult != UA_STATUSCODE_GOOD) ret = response.responseHeader.serviceResult; else if (response.resultsSize != 1) ret = UA_STATUSCODE_BADUNEXPECTEDERROR; else if (response.results[0].statusCode != UA_STATUSCODE_GOOD) ret = response.results[0].statusCode; else if (response.results[0].operationResultsSize != 0) ret = UA_STATUSCODE_BADUNEXPECTEDERROR; UA_HistoryUpdateResponse_clear(&response); return ret; } static UA_StatusCode updateHistory(UA_PerformUpdateType updateType, UA_DateTime *updateData, UA_StatusCode **operationResults, size_t *operationResultsSize) { UA_UpdateDataDetails *details = UA_UpdateDataDetails_new(); details->performInsertReplace = updateType; UA_NodeId_copy(&outNodeId, &details->nodeId); int updateDataSize = -1; while(updateData[++updateDataSize]); fprintf(stderr, "updateHistory for %d values.\n", updateDataSize); details->updateValuesSize = (size_t)updateDataSize; details->updateValues = (UA_DataValue*) UA_Array_new(details->updateValuesSize, &UA_TYPES[UA_TYPES_DATAVALUE]); for (size_t i = 0; i < details->updateValuesSize; ++i) { UA_DataValue_init(&details->updateValues[i]); details->updateValues[i].hasValue = true; UA_Int64 d = updateType; UA_Variant_setScalarCopy(&details->updateValues[i].value, &d, &UA_TYPES[UA_TYPES_INT64]); details->updateValues[i].hasSourceTimestamp = true; details->updateValues[i].sourceTimestamp = updateData[i]; details->updateValues[i].hasServerTimestamp = true; details->updateValues[i].serverTimestamp = updateData[i]; details->updateValues[i].hasStatus = true; details->updateValues[i].status = UA_STATUSCODE_GOOD; } UA_HistoryUpdateRequest request; UA_HistoryUpdateRequest_init(&request); request.historyUpdateDetailsSize = 1; request.historyUpdateDetails = UA_ExtensionObject_new(); UA_ExtensionObject_init(request.historyUpdateDetails); request.historyUpdateDetails[0].encoding = UA_EXTENSIONOBJECT_DECODED; request.historyUpdateDetails[0].content.decoded.type = &UA_TYPES[UA_TYPES_UPDATEDATADETAILS]; request.historyUpdateDetails[0].content.decoded.data = details; UA_HistoryUpdateResponse response; UA_HistoryUpdateResponse_init(&response); lockServer(server); Service_HistoryUpdate(server, &server->adminSession, &request, &response); unlockServer(server); UA_HistoryUpdateRequest_clear(&request); UA_StatusCode ret = UA_STATUSCODE_GOOD; if (response.responseHeader.serviceResult != UA_STATUSCODE_GOOD) ret = response.responseHeader.serviceResult; else if (response.resultsSize != 1) ret = UA_STATUSCODE_BADUNEXPECTEDERROR; else if (response.results[0].statusCode != UA_STATUSCODE_GOOD) ret = response.results[0].statusCode; else if (response.results[0].operationResultsSize != (size_t)updateDataSize) ret = UA_STATUSCODE_BADUNEXPECTEDERROR; else { if (operationResults) { *operationResultsSize = response.results[0].operationResultsSize; ret = UA_Array_copy(response.results[0].operationResults, *operationResultsSize, (void**)operationResults, &UA_TYPES[UA_TYPES_STATUSCODE]); } else { for (size_t i = 0; i < response.results[0].operationResultsSize; ++i) { if (response.results[0].operationResults[i] != UA_STATUSCODE_GOOD) { ret = response.results[0].operationResults[i]; break; } } } } UA_HistoryUpdateResponse_clear(&response); return ret; } static void testResult(UA_DateTime *resultData, UA_HistoryData * historyData) { // request UA_HistoryReadResponse localResponse; UA_HistoryReadResponse_init(&localResponse); requestHistory(TIMESTAMP_FIRST, TIMESTAMP_LAST, &localResponse, 0, false, NULL); // test the response ck_assert_str_eq(UA_StatusCode_name(localResponse.responseHeader.serviceResult), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(localResponse.resultsSize, 1); ck_assert_str_eq(UA_StatusCode_name(localResponse.results[0].statusCode), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(localResponse.results[0].historyData.encoding, UA_EXTENSIONOBJECT_DECODED); ck_assert(localResponse.results[0].historyData.content.decoded.type == &UA_TYPES[UA_TYPES_HISTORYDATA]); UA_HistoryData * data = (UA_HistoryData *)localResponse.results[0].historyData.content.decoded.data; if (historyData) UA_HistoryData_copy(data, historyData); for (size_t j = 0; j < data->dataValuesSize; ++j) { ck_assert(resultData[j] != 0); ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true); ck_assert_int_eq(data->dataValues[j].sourceTimestamp, resultData[j]); } UA_HistoryReadResponse_clear(&localResponse); } START_TEST(Server_HistorizingUpdateDelete) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill backend ck_assert_uint_eq(fillHistoricalDataBackend(backend), true); // delete some values ck_assert_str_eq(UA_StatusCode_name(deleteHistory(DELETE_START_TIME, DELETE_STOP_TIME)), UA_StatusCode_name(UA_STATUSCODE_GOOD)); testResult(testDataAfterDelete, NULL); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST #ifdef UA_ENABLE_MALLOC_SINGLETON START_TEST(Server_HistorizingReadHistoryDataOutOfMemory) { UA_ReadRawModifiedDetails details; UA_ReadRawModifiedDetails_init(&details); UA_HistoryReadValueId valueId; UA_HistoryReadValueId_init(&valueId); valueId.nodeId = outNodeId; UA_HistoryReadRequest request; UA_HistoryReadRequest_init(&request); request.historyReadDetails.encoding = UA_EXTENSIONOBJECT_DECODED; request.historyReadDetails.content.decoded.type = &UA_TYPES[UA_TYPES_READRAWMODIFIEDDETAILS]; request.historyReadDetails.content.decoded.data = &details; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH; request.nodesToReadSize = 1; request.nodesToRead = &valueId; UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); lockServer(server); UA_ServerConfig *config = UA_Server_getConfig(server); void (*readRaw)(UA_Server*, void*, const UA_NodeId*, void*, const UA_RequestHeader*, const UA_ReadRawModifiedDetails*, UA_TimestampsToReturn, UA_Boolean, size_t, const UA_HistoryReadValueId*, UA_HistoryReadResponse*, UA_HistoryData * const * const) = config->historyDatabase.readRaw; void *(*callocFunc)(size_t, size_t) = UA_callocSingleton; config->historyDatabase.readRaw = historyReadTestBackend; UA_callocSingleton = historyDataTestCalloc; failHistoryDataAllocation = true; historyReadBackendCalled = false; Service_HistoryRead(server, &server->adminSession, &request, &response); UA_callocSingleton = callocFunc; config->historyDatabase.readRaw = readRaw; unlockServer(server); ck_assert(!failHistoryDataAllocation); ck_assert(!historyReadBackendCalled); ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_BADOUTOFMEMORY); UA_HistoryReadResponse_clear(&response); } END_TEST START_TEST(Server_HistorizingUpdateResultsOutOfMemory) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_uint_eq(ret, UA_STATUSCODE_GOOD); UA_Int64 scalar = 1; UA_DataValue value; UA_DataValue_init(&value); UA_Variant_setScalar(&value.value, &scalar, &UA_TYPES[UA_TYPES_INT64]); value.hasValue = true; value.hasSourceTimestamp = true; value.sourceTimestamp = 1; UA_UpdateDataDetails details; UA_UpdateDataDetails_init(&details); details.nodeId = outNodeId; details.performInsertReplace = UA_PERFORMUPDATETYPE_INSERT; details.updateValuesSize = 1; details.updateValues = &value; UA_ExtensionObject updateDetails; UA_ExtensionObject_init(&updateDetails); updateDetails.encoding = UA_EXTENSIONOBJECT_DECODED; updateDetails.content.decoded.type = &UA_TYPES[UA_TYPES_UPDATEDATADETAILS]; updateDetails.content.decoded.data = &details; UA_HistoryUpdateRequest request; UA_HistoryUpdateRequest_init(&request); request.historyUpdateDetailsSize = 1; request.historyUpdateDetails = &updateDetails; UA_HistoryUpdateResponse response; UA_HistoryUpdateResponse_init(&response); lockServer(server); void *(*callocFunc)(size_t, size_t) = UA_callocSingleton; UA_callocSingleton = historyDataTestCalloc; failHistoryUpdateResultsAllocation = true; Service_HistoryUpdate(server, &server->adminSession, &request, &response); UA_callocSingleton = callocFunc; unlockServer(server); ck_assert(!failHistoryUpdateResultsAllocation); ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); ck_assert_uint_eq(response.resultsSize, 1); ck_assert_uint_eq(response.results[0].statusCode, UA_STATUSCODE_BADOUTOFMEMORY); ck_assert_uint_eq(response.results[0].operationResultsSize, 0); ck_assert(response.results[0].operationResults == NULL); UA_HistoryUpdateResponse_clear(&response); UA_HistoryDataBackend_Memory_clear(&backend); } END_TEST #endif START_TEST(Server_HistorizingUpdateInsert) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill backend with insert ck_assert_str_eq(UA_StatusCode_name(updateHistory(UA_PERFORMUPDATETYPE_INSERT, testData, NULL, NULL)) , UA_StatusCode_name(UA_STATUSCODE_GOOD)); UA_HistoryData data; UA_HistoryData_init(&data); testResult(testDataSorted, &data); for (size_t i = 0; i < data.dataValuesSize; ++i) { ck_assert_uint_eq(data.dataValues[i].hasValue, true); ck_assert(data.dataValues[i].value.type == &UA_TYPES[UA_TYPES_INT64]); ck_assert_int_eq(*((UA_Int64*)data.dataValues[i].value.data), UA_PERFORMUPDATETYPE_INSERT); } UA_HistoryData_clear(&data); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingUpdateReplace) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill backend with insert ck_assert_str_eq(UA_StatusCode_name(updateHistory(UA_PERFORMUPDATETYPE_INSERT, testData, NULL, NULL)) , UA_StatusCode_name(UA_STATUSCODE_GOOD)); // replace all ck_assert_str_eq(UA_StatusCode_name(updateHistory(UA_PERFORMUPDATETYPE_REPLACE, testData, NULL, NULL)) , UA_StatusCode_name(UA_STATUSCODE_GOOD)); UA_HistoryData data; UA_HistoryData_init(&data); testResult(testDataSorted, &data); for (size_t i = 0; i < data.dataValuesSize; ++i) { ck_assert_uint_eq(data.dataValues[i].hasValue, true); ck_assert(data.dataValues[i].value.type == &UA_TYPES[UA_TYPES_INT64]); ck_assert_int_eq(*((UA_Int64*)data.dataValues[i].value.data), UA_PERFORMUPDATETYPE_REPLACE); } UA_HistoryData_clear(&data); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingUpdateUpdate) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill backend with insert ck_assert_str_eq(UA_StatusCode_name(updateHistory(UA_PERFORMUPDATETYPE_INSERT, testData, NULL, NULL)) , UA_StatusCode_name(UA_STATUSCODE_GOOD)); testResult(testDataSorted, NULL); // delete some values ck_assert_str_eq(UA_StatusCode_name(deleteHistory(DELETE_START_TIME, DELETE_STOP_TIME)), UA_StatusCode_name(UA_STATUSCODE_GOOD)); testResult(testDataAfterDelete, NULL); // update all and insert some UA_StatusCode *result = NULL; size_t resultSize = 0; ck_assert_uint_eq(updateHistory(UA_PERFORMUPDATETYPE_UPDATE, testDataSorted, &result, &resultSize), UA_STATUSCODE_GOOD); for (size_t i = 0; i < resultSize; ++i) { ck_assert_str_eq(UA_StatusCode_name(result[i]), UA_StatusCode_name(testDataUpdateResult[i])); } UA_Array_delete(result, resultSize, &UA_TYPES[UA_TYPES_STATUSCODE]); UA_HistoryData data; UA_HistoryData_init(&data); testResult(testDataSorted, &data); for (size_t i = 0; i < data.dataValuesSize; ++i) { ck_assert_uint_eq(data.dataValues[i].hasValue, true); ck_assert(data.dataValues[i].value.type == &UA_TYPES[UA_TYPES_INT64]); ck_assert_int_eq(*((UA_Int64*)data.dataValues[i].value.data), UA_PERFORMUPDATETYPE_UPDATE); } UA_HistoryData_clear(&data); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingStrategyUser) { // set a data backend UA_HistorizingNodeIdSettings setting; setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100); setting.maxHistoryDataResponseSize = 100; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill the data UA_EventLoop *el = UA_Client_getConfig(client)->eventLoop; UA_DateTime start = el->dateTime_now(el); UA_DateTime end = start + (10 * UA_DATETIME_SEC); for (UA_UInt32 i = 0; i < 10; ++i) { UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; value.hasStatus = true; value.status = UA_STATUSCODE_GOOD; UA_Variant_setScalarCopy(&value.value, &i, &UA_TYPES[UA_TYPES_UINT32]); value.hasSourceTimestamp = true; value.sourceTimestamp = start + (i * UA_DATETIME_SEC); value.hasServerTimestamp = true; value.serverTimestamp = value.sourceTimestamp; retval = setting.historizingBackend.serverSetHistoryData(server, setting.historizingBackend.context, NULL, NULL, &outNodeId, UA_FALSE, &value); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); UA_DataValue_clear(&value); } // request UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); requestHistory(start, end, &response, 0, false, NULL); // test the response ck_assert_str_eq(UA_StatusCode_name(response.responseHeader.serviceResult), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(response.resultsSize, 1); for (size_t i = 0; i < response.resultsSize; ++i) { ck_assert_str_eq(UA_StatusCode_name(response.results[i].statusCode), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(response.results[i].historyData.encoding, UA_EXTENSIONOBJECT_DECODED); ck_assert(response.results[i].historyData.content.decoded.type == &UA_TYPES[UA_TYPES_HISTORYDATA]); UA_HistoryData * data = (UA_HistoryData *)response.results[i].historyData.content.decoded.data; ck_assert_uint_eq(data->dataValuesSize, 10); for (size_t j = 0; j < data->dataValuesSize; ++j) { ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true); ck_assert_int_eq(data->dataValues[j].sourceTimestamp, start + (UA_DateTime)(j * UA_DATETIME_SEC)); ck_assert_uint_eq(data->dataValues[j].hasStatus, true); ck_assert_str_eq(UA_StatusCode_name(data->dataValues[j].status), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(data->dataValues[j].hasValue, true); ck_assert(data->dataValues[j].value.type == &UA_TYPES[UA_TYPES_UINT32]); UA_UInt32 * value = (UA_UInt32 *)data->dataValues[j].value.data; ck_assert_uint_eq(*value, j); } } UA_HistoryReadResponse_clear(&response); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST /* A raw read with an IndexRange returns that range of each value. The parsed * range must be freed afterwards (LeakSanitizer reports it otherwise). */ START_TEST(Server_HistorizingReadIndexRange) { UA_HistorizingNodeIdSettings setting; setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100); setting.maxHistoryDataResponseSize = 100; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); UA_EventLoop *el = UA_Client_getConfig(client)->eventLoop; UA_DateTime start = el->dateTime_now(el); UA_DateTime end = start + (3 * UA_DATETIME_SEC); for(UA_UInt32 i = 0; i < 3; ++i) { UA_UInt32 elements[2] = {i, 100 + i}; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setArrayCopy(&value.value, elements, 2, &UA_TYPES[UA_TYPES_UINT32]); value.hasSourceTimestamp = true; value.sourceTimestamp = start + (i * UA_DATETIME_SEC); value.hasServerTimestamp = true; value.serverTimestamp = value.sourceTimestamp; retval = setting.historizingBackend. serverSetHistoryData(server, setting.historizingBackend.context, NULL, NULL, &outNodeId, UA_FALSE, &value); ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); UA_DataValue_clear(&value); } UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); requestHistoryRange(start, end, &response, 0, false, NULL, "1"); ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); ck_assert_uint_eq(response.resultsSize, 1); ck_assert_uint_eq(response.results[0].statusCode, UA_STATUSCODE_GOOD); UA_HistoryData *data = (UA_HistoryData *) response.results[0].historyData.content.decoded.data; ck_assert_ptr_ne(data, NULL); ck_assert_uint_eq(data->dataValuesSize, 3); for(size_t j = 0; j < data->dataValuesSize; ++j) { const UA_Variant *v = &data->dataValues[j].value; ck_assert(v->type == &UA_TYPES[UA_TYPES_UINT32]); ck_assert_uint_eq(v->arrayLength, 1); ck_assert_uint_eq(*(UA_UInt32 *)v->data, 100 + j); } UA_HistoryReadResponse_clear(&response); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingStrategyPoll) { UA_EventLoop *el = UA_Client_getConfig(client)->eventLoop; UA_DateTime start = el->dateTime_now(el); // init to a defined value UA_StatusCode retval = setUInt32(client, outNodeId, 43); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // set a data backend UA_HistorizingNodeIdSettings setting; setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100); setting.maxHistoryDataResponseSize = 100; setting.pollingInterval = 100; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_POLL; retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill the data retval = gathering->startPoll(server, gathering->context, &outNodeId); ck_assert_int_eq(retval, UA_STATUSCODE_GOOD); for(size_t k = 0; k < 10; ++k) { UA_atomic_store(&running, false); THREAD_JOIN(server_thread); UA_fakeSleep(setting.pollingInterval); UA_Server_run_iterate(server, false); UA_atomic_store(&running, true); THREAD_CREATE(server_thread, serverloop); if(k == 5) { gathering->stopPoll(server, gathering->context, &outNodeId); } setUInt32(client, outNodeId, (unsigned int)k); } ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); UA_DateTime end = el->dateTime_now(el); // request UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); requestHistory(start, end, &response, 0, false, NULL); // test the response ck_assert_int_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); ck_assert_uint_eq(response.resultsSize, 1); for(size_t i = 0; i < response.resultsSize; ++i) { ck_assert_int_eq(response.results[i].statusCode, UA_STATUSCODE_GOOD); ck_assert_uint_eq(response.results[i].historyData.encoding, UA_EXTENSIONOBJECT_DECODED); ck_assert_ptr_eq(response.results[i].historyData.content.decoded.type, &UA_TYPES[UA_TYPES_HISTORYDATA]); UA_HistoryData *data = (UA_HistoryData *) response.results[i].historyData.content.decoded.data; ck_assert(data->dataValuesSize > 1); for(size_t j = 0; j < data->dataValuesSize; ++j) { ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true); ck_assert(data->dataValues[j].sourceTimestamp >= start); ck_assert(data->dataValues[j].sourceTimestamp < end); ck_assert_uint_eq(data->dataValues[j].hasValue, true); ck_assert(data->dataValues[j].value.type == &UA_TYPES[UA_TYPES_UINT32]); UA_UInt32 * value = (UA_UInt32 *)data->dataValues[j].value.data; // first need to be 43 if(j == 0) { ck_assert_uint_eq(*value, 43); } else { ck_assert(*value < 5); } } } UA_HistoryReadResponse_clear(&response); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingStrategyValueSet) { // init to a defined value UA_StatusCode retval = setUInt32(client, outNodeId, 43); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // set a data backend UA_HistorizingNodeIdSettings setting; setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100); setting.maxHistoryDataResponseSize = 100; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET; retval = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // fill the data UA_fakeSleep(100); UA_EventLoop *el = UA_Client_getConfig(client)->eventLoop; UA_DateTime start = el->dateTime_now(el); UA_fakeSleep(100); for (UA_UInt32 i = 0; i < 10; ++i) { retval = setUInt32(client, outNodeId, i); ck_assert_str_eq(UA_StatusCode_name(retval), UA_StatusCode_name(UA_STATUSCODE_GOOD)); UA_fakeSleep(100); } UA_DateTime end = el->dateTime_now(el); // request UA_HistoryReadResponse response; UA_HistoryReadResponse_init(&response); requestHistory(start, end, &response, 0, false, NULL); // test the response ck_assert_str_eq(UA_StatusCode_name(response.responseHeader.serviceResult), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(response.resultsSize, 1); for (size_t i = 0; i < response.resultsSize; ++i) { ck_assert_str_eq(UA_StatusCode_name(response.results[i].statusCode), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(response.results[i].historyData.encoding, UA_EXTENSIONOBJECT_DECODED); ck_assert(response.results[i].historyData.content.decoded.type == &UA_TYPES[UA_TYPES_HISTORYDATA]); UA_HistoryData * data = (UA_HistoryData *)response.results[i].historyData.content.decoded.data; ck_assert(data->dataValuesSize > 0); for (size_t j = 0; j < data->dataValuesSize; ++j) { ck_assert(data->dataValues[j].sourceTimestamp >= start && data->dataValues[j].sourceTimestamp < end); ck_assert_uint_eq(data->dataValues[j].hasSourceTimestamp, true); ck_assert_str_eq(UA_StatusCode_name(data->dataValues[j].status), UA_StatusCode_name(UA_STATUSCODE_GOOD)); ck_assert_uint_eq(data->dataValues[j].hasValue, true); ck_assert(data->dataValues[j].value.type == &UA_TYPES[UA_TYPES_UINT32]); UA_UInt32 * value = (UA_UInt32 *)data->dataValues[j].value.data; ck_assert_uint_eq(*value, j); } } UA_HistoryReadResponse_clear(&response); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingBackendMemory) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // empty backend should not crash UA_UInt32 retval = testHistoricalDataBackend(100); fprintf(stderr, "%x tests expected failed.\n", retval); // fill backend ck_assert_uint_eq(fillHistoricalDataBackend(backend), true); // read all in one retval = testHistoricalDataBackend(100); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); // read continuous one at one request retval = testHistoricalDataBackend(1); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); // read continuous two at one request retval = testHistoricalDataBackend(2); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); UA_HistoryDataBackend_Memory_clear(&setting.historizingBackend); } END_TEST START_TEST(Server_HistorizingRandomIndexBackend) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_randomindextest(testData); UA_HistorizingNodeIdSettings setting; setting.historizingBackend = backend; setting.maxHistoryDataResponseSize = 1000; setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_USER; UA_StatusCode ret = gathering->registerNodeId(server, gathering->context, &outNodeId, setting); ck_assert_str_eq(UA_StatusCode_name(ret), UA_StatusCode_name(UA_STATUSCODE_GOOD)); // read all in one UA_UInt32 retval = testHistoricalDataBackend(100); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); // read continuous one at one request retval = testHistoricalDataBackend(1); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); // read continuous two at one request retval = testHistoricalDataBackend(2); fprintf(stderr, "%x tests failed.\n", retval); ck_assert_uint_eq(retval, 0); UA_HistoryDataBackend_randomindextest_clear(&backend); } END_TEST static void setupMemoryBackend(void) { server = UA_Server_newForUnitTest(); ck_assert_ptr_nonnull(server); } static void teardownMemoryBackend(void) { UA_Server_delete(server); } START_TEST(MemoryBackendGrowsNodeStorage) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(0, 0); ck_assert_ptr_nonnull(backend.context); char names[3][2] = {"A", "B", "C"}; for(UA_UInt32 i = 0; i < 3; i++) { UA_NodeId id = UA_NODEID_STRING(1, names[i]); UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &i, &UA_TYPES[UA_TYPES_UINT32]); value.hasServerTimestamp = true; value.serverTimestamp = 100 + i; ck_assert_uint_eq(backend.serverSetHistoryData(server, backend.context, NULL, NULL, &id, false, &value), UA_STATUSCODE_GOOD); } for(UA_UInt32 i = 0; i < 3; i++) { UA_NodeId id = UA_NODEID_STRING(1, names[i]); ck_assert_uint_eq(backend.getEnd(server, backend.context, NULL, NULL, &id), 1); const UA_DataValue *stored = backend.getDataValue(server, backend.context, NULL, NULL, &id, 0); ck_assert(UA_Variant_hasScalarType(&stored->value, &UA_TYPES[UA_TYPES_UINT32])); ck_assert_uint_eq(*(UA_UInt32*)stored->value.data, i); ck_assert(!stored->hasSourceTimestamp); ck_assert(stored->hasServerTimestamp); ck_assert_int_eq(stored->serverTimestamp, 100 + i); ck_assert(backend.timestampsToReturnSupported(server, backend.context, NULL, NULL, &id, UA_TIMESTAMPSTORETURN_SERVER)); ck_assert(!backend.timestampsToReturnSupported(server, backend.context, NULL, NULL, &id, UA_TIMESTAMPSTORETURN_SOURCE)); ck_assert(!backend.timestampsToReturnSupported(server, backend.context, NULL, NULL, &id, UA_TIMESTAMPSTORETURN_BOTH)); } /* The backend owns its NodeIds independently of the caller's storage. */ memset(names, 0, sizeof(names)); UA_NodeId first = UA_NODEID_STRING(1, "A"); ck_assert_uint_eq(backend.getEnd(server, backend.context, NULL, NULL, &first), 1); backend.deleteMembers(&backend); } END_TEST START_TEST(MemoryBackendRejectsMissingUpdateTimestamp) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); ck_assert_ptr_nonnull(backend.context); UA_NodeId id = UA_NODEID_NUMERIC(1, 100); UA_UInt32 number = 7; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &number, &UA_TYPES[UA_TYPES_UINT32]); value.hasSourceTimestamp = true; value.sourceTimestamp = 100; ck_assert_uint_eq(backend.insertDataValue(server, backend.context, NULL, NULL, &id, &value), UA_STATUSCODE_GOOD); value.hasSourceTimestamp = false; number = 99; UA_StatusCode res; if(_i == 0) res = backend.insertDataValue(server, backend.context, NULL, NULL, &id, &value); else if(_i == 1) res = backend.replaceDataValue(server, backend.context, NULL, NULL, &id, &value); else res = backend.updateDataValue(server, backend.context, NULL, NULL, &id, &value); ck_assert_uint_eq(res, UA_STATUSCODE_BADINVALIDTIMESTAMP); ck_assert_uint_eq(backend.getEnd(server, backend.context, NULL, NULL, &id), 1); const UA_DataValue *stored = backend.getDataValue(server, backend.context, NULL, NULL, &id, 0); ck_assert_uint_eq(*(UA_UInt32*)stored->value.data, 7); ck_assert(stored->hasSourceTimestamp && stored->hasServerTimestamp); ck_assert_int_eq(stored->sourceTimestamp, 100); ck_assert_int_eq(stored->serverTimestamp, 100); backend.deleteMembers(&backend); } END_TEST START_TEST(MemoryBackendCompletesReplacementTimestamp) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 1); ck_assert_ptr_nonnull(backend.context); UA_NodeId id = UA_NODEID_NUMERIC(1, 100); UA_UInt32 number = 7; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &number, &UA_TYPES[UA_TYPES_UINT32]); value.hasServerTimestamp = true; value.serverTimestamp = 100; ck_assert_uint_eq(backend.insertDataValue(server, backend.context, NULL, NULL, &id, &value), UA_STATUSCODE_GOOD); value.hasServerTimestamp = false; value.hasSourceTimestamp = true; value.sourceTimestamp = 100; number = 99; ck_assert_uint_eq(backend.replaceDataValue(server, backend.context, NULL, NULL, &id, &value), UA_STATUSCODE_GOOD); const UA_DataValue *stored = backend.getDataValue(server, backend.context, NULL, NULL, &id, 0); ck_assert_uint_eq(*(UA_UInt32*)stored->value.data, 99); ck_assert(stored->hasSourceTimestamp && stored->hasServerTimestamp); ck_assert_int_eq(stored->sourceTimestamp, 100); ck_assert_int_eq(stored->serverTimestamp, 100); backend.deleteMembers(&backend); } END_TEST START_TEST(MemoryBackendRangeFailurePreservesCompletedValues) { const UA_Boolean reverse = (_i & 1) != 0; const UA_Boolean missingValue = (_i & 2) != 0; const UA_Boolean omitCount = (_i & 4) != 0; UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory(1, 2); ck_assert_ptr_nonnull(backend.context); UA_NodeId id = UA_NODEID_NUMERIC(1, 100); UA_UInt32 numbers[2] = {10, 20}; for(size_t i = 0; i < 2; i++) { UA_Boolean valid = (i == (reverse ? 1u : 0u)); UA_DataValue value; UA_DataValue_init(&value); value.hasSourceTimestamp = true; value.sourceTimestamp = (UA_DateTime)(100 + i); if(valid || !missingValue) { value.hasValue = true; UA_Variant_setArray(&value.value, numbers, valid ? 2 : 1, &UA_TYPES[UA_TYPES_UINT32]); } else { value.hasStatus = true; value.status = UA_STATUSCODE_BADWAITINGFORINITIALDATA; } ck_assert_uint_eq(backend.serverSetHistoryData(server, backend.context, NULL, NULL, &id, false, &value), UA_STATUSCODE_GOOD); } UA_NumericRangeDimension dimension = {1, 1}; UA_NumericRange range = {1, &dimension}; UA_ByteString continuation = UA_BYTESTRING_NULL; UA_ByteString next = UA_BYTESTRING_NULL; UA_DataValue results[2]; UA_DataValue_init(&results[0]); UA_DataValue_init(&results[1]); size_t provided = SIZE_MAX; UA_StatusCode res = backend.copyDataValues(server, backend.context, NULL, NULL, &id, reverse ? 1 : 0, reverse ? 0 : 1, reverse, 2, range, false, &continuation, &next, omitCount ? NULL : &provided, results); ck_assert_uint_eq(res, missingValue ? UA_STATUSCODE_BADDATAUNAVAILABLE : UA_STATUSCODE_BADINDEXRANGENODATA); if(!omitCount) ck_assert_uint_eq(provided, 1); ck_assert(results[0].hasValue); ck_assert(UA_Variant_hasArrayType(&results[0].value, &UA_TYPES[UA_TYPES_UINT32])); ck_assert_uint_eq(results[0].value.arrayLength, 1); ck_assert_uint_eq(*(UA_UInt32*)results[0].value.data, 20); ck_assert(UA_Variant_isEmpty(&results[1].value)); ck_assert_uint_eq(next.length, 0); UA_DataValue_clear(&results[0]); UA_DataValue_clear(&results[1]); UA_ByteString_clear(&next); backend.deleteMembers(&backend); } END_TEST START_TEST(MemoryBackendRejectsMalformedContinuationPoint) { UA_HistoryDataBackend backend; if(_i) backend = UA_HistoryDataBackend_Memory_Circular(1, 1); else backend = UA_HistoryDataBackend_Memory(1, 1); ck_assert_ptr_nonnull(backend.context); UA_NodeId id = UA_NODEID_NUMERIC(1, 100); UA_Byte byte = 0; UA_ByteString continuation = {1, &byte}; UA_ByteString next = UA_BYTESTRING_NULL; UA_NumericRange range = {0, NULL}; UA_DataValue value; UA_DataValue_init(&value); size_t provided = SIZE_MAX; UA_StatusCode res; if(_i) { UA_HistoryData history; UA_HistoryData_init(&history); res = backend.getHistoryData(server, NULL, NULL, &backend, 0, 0, &id, 1, 1, false, UA_TIMESTAMPSTORETURN_BOTH, range, false, &continuation, &next, &history); ck_assert_uint_eq(history.dataValuesSize, 0); ck_assert_ptr_null(history.dataValues); UA_HistoryData_clear(&history); } else { res = backend.copyDataValues(server, backend.context, NULL, NULL, &id, 0, 0, false, 1, range, false, &continuation, &next, &provided, &value); } ck_assert_uint_eq(res, UA_STATUSCODE_BADCONTINUATIONPOINTINVALID); ck_assert_uint_eq(provided, SIZE_MAX); ck_assert(UA_Variant_isEmpty(&value.value)); ck_assert_uint_eq(next.length, 0); backend.deleteMembers(&backend); } END_TEST START_TEST(MemoryBackendCompletesMissingTimestamps) { UA_HistoryDataBackend backend; if(_i) backend = UA_HistoryDataBackend_Memory_Circular(1, 1); else backend = UA_HistoryDataBackend_Memory(1, 1); ck_assert_ptr_nonnull(backend.context); UA_NodeId id = UA_NODEID_NUMERIC(1, 100); UA_UInt32 number = 7; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &number, &UA_TYPES[UA_TYPES_UINT32]); ck_assert_uint_eq(backend.serverSetHistoryData(server, backend.context, NULL, NULL, &id, false, &value), UA_STATUSCODE_GOOD); const UA_DataValue *stored = backend.getDataValue(server, backend.context, NULL, NULL, &id, 0); ck_assert(stored->hasServerTimestamp); ck_assert_int_gt(stored->serverTimestamp, 0); ck_assert(!stored->hasSourceTimestamp); ck_assert_uint_eq(*(UA_UInt32*)stored->value.data, 7); backend.deleteMembers(&backend); } END_TEST START_TEST(CircularBackendRejectsExcessNodes) { UA_HistoryDataBackend backend = UA_HistoryDataBackend_Memory_Circular(1, 1); ck_assert_ptr_nonnull(backend.context); UA_NodeId first = UA_NODEID_NUMERIC(1, 100); UA_NodeId second = UA_NODEID_NUMERIC(1, 101); UA_UInt32 number = 7; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &number, &UA_TYPES[UA_TYPES_UINT32]); value.hasServerTimestamp = true; value.serverTimestamp = 100; ck_assert_uint_eq(backend.serverSetHistoryData(server, backend.context, NULL, NULL, &first, false, &value), UA_STATUSCODE_GOOD); ck_assert_uint_eq(backend.serverSetHistoryData(server, backend.context, NULL, NULL, &second, false, &value), UA_STATUSCODE_BADOUTOFMEMORY); ck_assert_uint_eq(backend.getEnd(server, backend.context, NULL, NULL, &first), 1); const UA_DataValue *stored = backend.getDataValue(server, backend.context, NULL, NULL, &first, 0); ck_assert_uint_eq(*(UA_UInt32*)stored->value.data, 7); ck_assert_int_eq(stored->serverTimestamp, 100); backend.deleteMembers(&backend); } END_TEST static Suite * testSuite_Client(void) { Suite *s = suite_create("Server Historical Data"); TCase *tc_server = tcase_create("Server Historical Data Basic"); tcase_add_checked_fixture(tc_server, setup, teardown); tcase_add_test(tc_server, Server_HistorizingStrategyPoll); tcase_add_test(tc_server, Server_HistorizingStrategyUser); tcase_add_test(tc_server, Server_HistorizingReadIndexRange); tcase_add_test(tc_server, Server_HistorizingStrategyValueSet); tcase_add_test(tc_server, Server_HistorizingBackendMemory); tcase_add_test(tc_server, Server_HistorizingRandomIndexBackend); tcase_add_test(tc_server, Server_HistorizingUpdateDelete); tcase_add_test(tc_server, Server_HistorizingUpdateInsert); tcase_add_test(tc_server, Server_HistorizingUpdateReplace); tcase_add_test(tc_server, Server_HistorizingUpdateUpdate); suite_add_tcase(s, tc_server); TCase *tc_memory = tcase_create("Memory backend boundaries"); tcase_add_checked_fixture(tc_memory, setupMemoryBackend, teardownMemoryBackend); tcase_add_test(tc_memory, MemoryBackendGrowsNodeStorage); tcase_add_loop_test(tc_memory, MemoryBackendRejectsMissingUpdateTimestamp, 0, 3); tcase_add_test(tc_memory, MemoryBackendCompletesReplacementTimestamp); tcase_add_loop_test(tc_memory, MemoryBackendRangeFailurePreservesCompletedValues, 0, 8); tcase_add_loop_test(tc_memory, MemoryBackendRejectsMalformedContinuationPoint, 0, 2); tcase_add_loop_test(tc_memory, MemoryBackendCompletesMissingTimestamps, 0, 2); tcase_add_test(tc_memory, CircularBackendRejectsExcessNodes); suite_add_tcase(s, tc_memory); #ifdef UA_ENABLE_MALLOC_SINGLETON TCase *tc_oom = tcase_create("Server Historical Data Out Of Memory"); tcase_add_checked_fixture(tc_oom, setup, teardown); tcase_add_test(tc_oom, Server_HistorizingReadHistoryDataOutOfMemory); tcase_add_test(tc_oom, Server_HistorizingUpdateResultsOutOfMemory); suite_add_tcase(s, tc_oom); #endif return s; } int main(void) { testDataSorted = sortDateTimes(testData); Suite *s = testSuite_Client(); SRunner *sr = srunner_create(s); srunner_set_fork_status(sr, CK_NOFORK); srunner_run_all(sr,CK_NORMAL); int number_failed = srunner_ntests_failed(sr); srunner_free(sr); UA_free(testDataSorted); return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; }