From bb2b7ef7b7ba0971b71902a7713200733c19b7ba Mon Sep 17 00:00:00 2001 From: Andreas Ebner Date: Wed, 11 Mar 2026 00:23:47 +0100 Subject: [PATCH] tests(client): add coverage tests for client sources --- src/client/ua_client_util.c | 1 + tests/client/check_client.c | 323 +++++++++++++++++ tests/client/check_client_async_connect.c | 91 +++++ tests/client/check_client_discovery.c | 89 +++++ tests/client/check_client_highlevel.c | 199 ++++++++++ tests/client/check_client_subscriptions.c | 340 ++++++++++++++++++ .../check_client_subscriptions_datachange.c | 70 +++- 7 files changed, 1110 insertions(+), 3 deletions(-) diff --git a/src/client/ua_client_util.c b/src/client/ua_client_util.c index 1adc04e74..d812ba715 100644 --- a/src/client/ua_client_util.c +++ b/src/client/ua_client_util.c @@ -161,6 +161,7 @@ getRemoteDataTypes(UA_Client *client, UA_ReadRequest *req, *outCustomTypes = dta; + UA_ReadResponse_clear(&rr); UA_ReadRequest_clear(req); if(dta->typesSize == 0) { UA_free(dta->types); diff --git a/tests/client/check_client.c b/tests/client/check_client.c index 6407a5ef9..c10730a1b 100644 --- a/tests/client/check_client.c +++ b/tests/client/check_client.c @@ -451,6 +451,312 @@ START_TEST(Client_closes_on_server_error) { } END_TEST +static void timedCallbackFired(UA_Client *c, void *data) { + UA_Boolean *flag = (UA_Boolean *)data; + *flag = true; +} + +START_TEST(Client_addTimedCallback) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Boolean fired = false; + UA_UInt64 cbId = 0; + /* Use a deadline in the past so it fires on the next iteration. + * The event loop uses UA_DateTime_now_fake which starts at a low value. */ + retval = UA_Client_addTimedCallback(client, timedCallbackFired, + &fired, 1, &cbId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(cbId != 0); + + /* Iterate enough for the timer to fire */ + for(int i = 0; i < 20; i++) { + UA_fakeSleep(10); + UA_Client_run_iterate(client, 1); + } + ck_assert(fired == true); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_addRepeatedCallback) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Boolean fired = false; + UA_UInt64 cbId = 0; + retval = UA_Client_addRepeatedCallback(client, timedCallbackFired, + &fired, 50.0, &cbId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(cbId != 0); + + for(int i = 0; i < 20; i++) { + UA_fakeSleep(10); + UA_Client_run_iterate(client, 1); + } + ck_assert(fired == true); + + /* Remove the callback */ + UA_Client_removeCallback(client, cbId); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_changeRepeatedCallbackInterval) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Boolean fired = false; + UA_UInt64 cbId = 0; + retval = UA_Client_addRepeatedCallback(client, timedCallbackFired, + &fired, 5000.0, &cbId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Change to a shorter interval */ + retval = UA_Client_changeRepeatedCallbackInterval(client, cbId, 20.0); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + for(int i = 0; i < 20; i++) { + UA_fakeSleep(10); + UA_Client_run_iterate(client, 1); + } + ck_assert(fired == true); + + UA_Client_removeCallback(client, cbId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getConnectionAttribute) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Read serverDescription */ + UA_Variant val; + UA_QualifiedName key = {0, UA_STRING_STATIC("serverDescription")}; + retval = UA_Client_getConnectionAttribute(client, key, &val); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(val.type == &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); + + /* Read securityPolicyUri */ + UA_Variant val2; + UA_QualifiedName key2 = {0, UA_STRING_STATIC("securityPolicyUri")}; + retval = UA_Client_getConnectionAttribute(client, key2, &val2); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(val2.type == &UA_TYPES[UA_TYPES_STRING]); + + /* Read securityMode */ + UA_Variant val3; + UA_QualifiedName key3 = {0, UA_STRING_STATIC("securityMode")}; + retval = UA_Client_getConnectionAttribute(client, key3, &val3); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(val3.type == &UA_TYPES[UA_TYPES_MESSAGESECURITYMODE]); + + /* Read unknown key */ + UA_Variant val4; + UA_QualifiedName key4 = {0, UA_STRING_STATIC("unknownKey")}; + retval = UA_Client_getConnectionAttribute(client, key4, &val4); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINTERNALERROR); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getConnectionAttributeCopy) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Variant val; + UA_QualifiedName key = {0, UA_STRING_STATIC("serverDescription")}; + retval = UA_Client_getConnectionAttributeCopy(client, key, &val); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(val.type == &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); + UA_Variant_clear(&val); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getConnectionAttribute_scalar) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Read securityMode as scalar */ + UA_MessageSecurityMode mode; + UA_QualifiedName key = {0, UA_STRING_STATIC("securityMode")}; + retval = UA_Client_getConnectionAttribute_scalar(client, key, + &UA_TYPES[UA_TYPES_MESSAGESECURITYMODE], &mode); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Type mismatch path */ + UA_Int32 wrongType; + retval = UA_Client_getConnectionAttribute_scalar(client, key, + &UA_TYPES[UA_TYPES_INT32], &wrongType); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADNOTFOUND); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getNamespaceIndex) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Read existing namespace (ns=0 is always "http://opcfoundation.org/UA/") */ + UA_ReadRequest rreq; + UA_ReadRequest_init(&rreq); + UA_ReadValueId rvi; + UA_ReadValueId_init(&rvi); + rvi.nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_NAMESPACEARRAY); + rvi.attributeId = UA_ATTRIBUTEID_VALUE; + rreq.nodesToRead = &rvi; + rreq.nodesToReadSize = 1; + UA_ReadResponse rresp = UA_Client_Service_read(client, rreq); + ck_assert_uint_eq(rresp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_ReadResponse_clear(&rresp); + + /* Add and look up a namespace */ + UA_String nsUri = UA_STRING("http://test.namespace.example.com/"); + UA_UInt16 nsIndex = 0; + retval = UA_Client_addNamespace(client, nsUri, &nsIndex); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(nsIndex > 0); + + /* Look up again — should find it */ + UA_UInt16 nsIndex2 = 0; + retval = UA_Client_getNamespaceIndex(client, nsUri, &nsIndex2); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_uint_eq(nsIndex, nsIndex2); + + /* Add same namespace — should return existing index */ + UA_UInt16 nsIndex3 = 0; + retval = UA_Client_addNamespace(client, nsUri, &nsIndex3); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_uint_eq(nsIndex, nsIndex3); + + /* Non-existent namespace */ + UA_String unknown = UA_STRING("http://nonexistent/"); + UA_UInt16 nsIndex4 = 0; + retval = UA_Client_getNamespaceIndex(client, unknown, &nsIndex4); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADNOTFOUND); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_findDataType) { + UA_Client *client = UA_Client_newForUnitTest(); + + /* Known built-in type */ + const UA_DataType *dt = + UA_Client_findDataType(client, &UA_TYPES[UA_TYPES_INT32].typeId); + ck_assert_ptr_ne(dt, NULL); + ck_assert(UA_NodeId_equal(&dt->typeId, &UA_TYPES[UA_TYPES_INT32].typeId)); + + /* Unknown type */ + UA_NodeId unknownId = UA_NODEID_NUMERIC(99, 99999); + dt = UA_Client_findDataType(client, &unknownId); + ck_assert_ptr_eq(dt, NULL); + + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getState) { + UA_Client *client = UA_Client_newForUnitTest(); + + /* Before connecting */ + UA_SecureChannelState channelState; + UA_SessionState sessionState; + UA_StatusCode connectStatus; + UA_Client_getState(client, &channelState, &sessionState, &connectStatus); + ck_assert_uint_eq(channelState, UA_SECURECHANNELSTATE_CLOSED); + ck_assert_uint_eq(sessionState, UA_SESSIONSTATE_CLOSED); + ck_assert_uint_eq(connectStatus, UA_STATUSCODE_GOOD); + + /* With NULL outputs */ + UA_Client_getState(client, NULL, NULL, NULL); + + /* After connecting */ + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Client_getState(client, &channelState, &sessionState, &connectStatus); + ck_assert_uint_eq(channelState, UA_SECURECHANNELSTATE_OPEN); + ck_assert_uint_eq(sessionState, UA_SESSIONSTATE_ACTIVATED); + ck_assert_uint_eq(connectStatus, UA_STATUSCODE_GOOD); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_cancelByRequestHandle) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Cancel a non-existent request handle — server should still return GOOD + * (the CancelResponse itself succeeds, cancelCount is 0) */ + UA_UInt32 cancelCount = 99; + retval = UA_Client_cancelByRequestHandle(client, 999999, &cancelCount); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_uint_eq(cancelCount, 0); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_cancelByRequestId_notFound) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Request id not in the async list -> BADNOTFOUND */ + UA_UInt32 cancelCount = 0; + retval = UA_Client_cancelByRequestId(client, 999999, &cancelCount); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADNOTFOUND); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_setAuthenticationUsername) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_ClientConfig *cc = UA_Client_getConfig(client); + + UA_StatusCode retval = + UA_ClientConfig_setAuthenticationUsername(cc, "user1", "password"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(cc->userIdentityToken.content.decoded.type == + &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]); + + retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + static Suite* testSuite_Client(void) { Suite *s = suite_create("Client"); TCase *tc_client = tcase_create("Client Basic"); @@ -475,6 +781,23 @@ static Suite* testSuite_Client(void) { tcase_add_test(tc_client_reconnect, Client_activateSessionTimeout); tcase_add_test(tc_client_reconnect, Client_activateSessionLocaleIds); suite_add_tcase(s,tc_client_reconnect); + + TCase *tc_ext = tcase_create("Client Extended"); + tcase_add_checked_fixture(tc_ext, setup, teardown); + tcase_add_test(tc_ext, Client_addTimedCallback); + tcase_add_test(tc_ext, Client_addRepeatedCallback); + tcase_add_test(tc_ext, Client_changeRepeatedCallbackInterval); + tcase_add_test(tc_ext, Client_getConnectionAttribute); + tcase_add_test(tc_ext, Client_getConnectionAttributeCopy); + tcase_add_test(tc_ext, Client_getConnectionAttribute_scalar); + tcase_add_test(tc_ext, Client_getNamespaceIndex); + tcase_add_test(tc_ext, Client_findDataType); + tcase_add_test(tc_ext, Client_getState); + tcase_add_test(tc_ext, Client_cancelByRequestHandle); + tcase_add_test(tc_ext, Client_cancelByRequestId_notFound); + tcase_add_test(tc_ext, Client_setAuthenticationUsername); + suite_add_tcase(s, tc_ext); + return s; } diff --git a/tests/client/check_client_async_connect.c b/tests/client/check_client_async_connect.c index 549c03662..3618cbfd2 100644 --- a/tests/client/check_client_async_connect.c +++ b/tests/client/check_client_async_connect.c @@ -187,6 +187,94 @@ START_TEST(Client_run_iterate) { } END_TEST +START_TEST(Client_connectSecureChannelAsync) { + /* Connect only the SecureChannel without opening a Session */ + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connectSecureChannelAsync(client, + "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Drive both sides until the channel is open */ + while(client->channel.state != UA_SECURECHANNELSTATE_OPEN) { + UA_Server_run_iterate(server, false); + UA_Client_run_iterate(client, 0); + } + + UA_SecureChannelState channelState; + UA_SessionState sessionState; + UA_StatusCode connectStatus; + UA_Client_getState(client, &channelState, &sessionState, &connectStatus); + ck_assert_uint_eq(channelState, UA_SECURECHANNELSTATE_OPEN); + /* No session should be created */ + ck_assert(sessionState != UA_SESSIONSTATE_ACTIVATED); + + UA_Client_disconnectAsync(client); + while(client->channel.state != UA_SECURECHANNELSTATE_CLOSED) { + UA_Server_run_iterate(server, false); + UA_Client_run_iterate(client, 0); + } + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_connect_async_wrongUrl) { + /* Attempt to connect to a wrong port — should fail gracefully */ + UA_Client *client = UA_Client_newForUnitTest(); + UA_ClientConfig *cc = UA_Client_getConfig(client); + cc->timeout = 200; /* short timeout */ + UA_StatusCode retval = UA_Client_connectAsync(client, "opc.tcp://localhost:4841"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Drive a few iterations */ + for(int i = 0; i < 50; i++) { + UA_fakeSleep(10); + UA_Client_run_iterate(client, 0); + } + + UA_SecureChannelState channelState; + UA_StatusCode connectStatus; + UA_Client_getState(client, &channelState, NULL, &connectStatus); + /* Either still connecting or failed — not OPEN */ + ck_assert(channelState != UA_SECURECHANNELSTATE_OPEN || + connectStatus != UA_STATUSCODE_GOOD); + + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_connectUsername_async) { + /* Connect with username/password via the sync connectUsername wrapper, + * but using manual iteration for the server side */ + UA_Client *client = UA_Client_newForUnitTest(); + UA_ClientConfig *cc = UA_Client_getConfig(client); + cc->stateCallback = currentState; + connected = false; + UA_StatusCode retval = UA_Client_connectAsync(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + while(!connected) { + UA_Server_run_iterate(server, false); + retval = UA_Client_run_iterate(client, 0); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + } + + /* Verify the client is connected with an active session */ + UA_SecureChannelState channelState; + UA_SessionState sessionState; + UA_Client_getState(client, &channelState, &sessionState, NULL); + ck_assert_uint_eq(channelState, UA_SECURECHANNELSTATE_OPEN); + ck_assert_uint_eq(sessionState, UA_SESSIONSTATE_ACTIVATED); + + /* Disconnect asynchronously */ + UA_Client_disconnectAsync(client); + while(client->channel.state != UA_SECURECHANNELSTATE_CLOSED) { + UA_Server_run_iterate(server, false); + UA_Client_run_iterate(client, 0); + } + UA_Client_delete(client); +} +END_TEST + static Suite* testSuite_Client(void) { Suite *s = suite_create("Client"); TCase *tc_client_connect = tcase_create("Client Connect Async"); @@ -196,6 +284,9 @@ static Suite* testSuite_Client(void) { tcase_add_test(tc_client_connect, Client_no_connection); tcase_add_test(tc_client_connect, Client_without_run_iterate); tcase_add_test(tc_client_connect, Client_run_iterate); + tcase_add_test(tc_client_connect, Client_connectSecureChannelAsync); + tcase_add_test(tc_client_connect, Client_connect_async_wrongUrl); + tcase_add_test(tc_client_connect, Client_connectUsername_async); suite_add_tcase(s,tc_client_connect); return s; } diff --git a/tests/client/check_client_discovery.c b/tests/client/check_client_discovery.c index b49adf68a..523bd823a 100644 --- a/tests/client/check_client_discovery.c +++ b/tests/client/check_client_discovery.c @@ -60,11 +60,100 @@ START_TEST(Client_connect_badEndpointUrl) { } END_TEST +START_TEST(Client_getEndpoints) { + UA_Client *client = UA_Client_newForUnitTest(); + + size_t endpointCount = 0; + UA_EndpointDescription *endpoints = NULL; + UA_StatusCode retval = UA_Client_getEndpoints(client, + "opc.tcp://localhost:4840", + &endpointCount, &endpoints); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(endpointCount > 0); + ck_assert_ptr_ne(endpoints, NULL); + + /* Verify each endpoint has a non-empty securityPolicyUri */ + for(size_t i = 0; i < endpointCount; i++) { + ck_assert(endpoints[i].securityPolicyUri.length > 0); + } + + UA_Array_delete(endpoints, endpointCount, + &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_findServers) { + UA_Client *client = UA_Client_newForUnitTest(); + + size_t serverCount = 0; + UA_ApplicationDescription *servers = NULL; + UA_StatusCode retval = UA_Client_findServers(client, + "opc.tcp://localhost:4840", + 0, NULL, 0, NULL, + &serverCount, &servers); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(serverCount > 0); + ck_assert_ptr_ne(servers, NULL); + + UA_Array_delete(servers, serverCount, + &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); + + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_getEndpoints_connected) { + /* Connect first, then getEndpoints on same URL — should succeed */ + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + size_t endpointCount = 0; + UA_EndpointDescription *endpoints = NULL; + retval = UA_Client_getEndpoints(client, "opc.tcp://localhost:4840", + &endpointCount, &endpoints); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(endpointCount > 0); + + UA_Array_delete(endpoints, endpointCount, + &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_findServers_connected) { + /* Connect first, then findServers on same URL — should succeed */ + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + size_t serverCount = 0; + UA_ApplicationDescription *servers = NULL; + retval = UA_Client_findServers(client, "opc.tcp://localhost:4840", + 0, NULL, 0, NULL, + &serverCount, &servers); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(serverCount > 0); + + UA_Array_delete(servers, serverCount, + &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + static Suite* testSuite_Client(void) { Suite *s = suite_create("Client"); TCase *tc_client = tcase_create("Client Discovery"); tcase_add_checked_fixture(tc_client, setup, teardown); tcase_add_test(tc_client, Client_connect_badEndpointUrl); + tcase_add_test(tc_client, Client_getEndpoints); + tcase_add_test(tc_client, Client_findServers); + tcase_add_test(tc_client, Client_getEndpoints_connected); + tcase_add_test(tc_client, Client_findServers_connected); suite_add_tcase(s,tc_client); return s; } diff --git a/tests/client/check_client_highlevel.c b/tests/client/check_client_highlevel.c index cfac61e52..a59e3f211 100644 --- a/tests/client/check_client_highlevel.c +++ b/tests/client/check_client_highlevel.c @@ -1034,6 +1034,188 @@ END_TEST #endif +/* Extended tests for untested highlevel functions */ + +START_TEST(Highlevel_TranslateBrowsePath) { + /* Translate a browse path from Objects folder to Server node */ + UA_BrowsePath bp; + UA_BrowsePath_init(&bp); + bp.startingNode = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); + + UA_RelativePathElement rpe; + UA_RelativePathElement_init(&rpe); + rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); + rpe.targetName = UA_QUALIFIEDNAME(0, "Server"); + bp.relativePath.elementsSize = 1; + bp.relativePath.elements = &rpe; + + UA_BrowsePathResult res = + UA_Client_translateBrowsePathToNodeIds(client, &bp); + ck_assert_uint_eq(res.statusCode, UA_STATUSCODE_GOOD); + ck_assert(res.targetsSize > 0); + UA_NodeId serverNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER); + ck_assert(UA_NodeId_equal(&res.targets[0].targetId.nodeId, + &serverNodeId)); + UA_BrowsePathResult_clear(&res); +} +END_TEST + +START_TEST(Highlevel_WriteValueScalar) { + /* Write a scalar value using writeValueAttribute_scalar and read back */ + UA_Int32 newVal = 42; + UA_StatusCode retval = + UA_Client_writeValueAttribute_scalar(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE), + &newVal, &UA_TYPES[UA_TYPES_INT32]); + /* Access may be denied because serverstatus/state is read-only, + * but the function path is exercised */ + (void)retval; +} +END_TEST + +START_TEST(Highlevel_WriteValueEx) { + /* Write a DataValue using writeValueAttributeEx and read back */ + UA_DataValue dv; + UA_DataValue_init(&dv); + dv.hasValue = true; + UA_Int32 val = 99; + UA_Variant_setScalar(&dv.value, &val, &UA_TYPES[UA_TYPES_INT32]); + dv.hasSourceTimestamp = true; + dv.sourceTimestamp = UA_DateTime_now(); + + UA_StatusCode retval = + UA_Client_writeValueAttributeEx(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE), + &dv); + /* May be denied on read-only node but exercises the code path */ + (void)retval; +} +END_TEST + +#ifdef UA_ENABLE_NODEMANAGEMENT +START_TEST(Highlevel_ReadWriteAccessLevelEx) { + /* Create a variable node to test AccessLevelEx */ + UA_VariableAttributes attr = UA_VariableAttributes_default; + attr.displayName = UA_LOCALIZEDTEXT("en-US", "ALExTest"); + attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; + attr.writeMask = 0xFFFFFFFF; + UA_Int32 intVal = 123; + UA_Variant_setScalar(&attr.value, &intVal, &UA_TYPES[UA_TYPES_INT32]); + attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId; + UA_NodeId outId = UA_NODEID_NULL; + UA_StatusCode retval = + UA_Client_addVariableNode(client, UA_NODEID_NULL, + UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), + UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), + UA_QUALIFIEDNAME(1, "ALExTest"), + UA_NODEID_NULL, attr, &outId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Read AccessLevelEx */ + UA_UInt32 alEx = 0; + retval = UA_Client_readAccessLevelExAttribute(client, outId, &alEx); + /* May not be supported → either GOOD or BADATTRIBUTEIDINVALID */ + if(retval == UA_STATUSCODE_GOOD) { + /* Write AccessLevelEx */ + UA_UInt32 newAlEx = alEx | 0x100; /* set a higher bit */ + retval = UA_Client_writeAccessLevelExAttribute(client, outId, &newAlEx); + /* May fail, but exercises the path */ + (void)retval; + } + + /* Cleanup */ + UA_Client_deleteNode(client, outId, true); + UA_NodeId_clear(&outId); +} +END_TEST +#endif + +START_TEST(Highlevel_ForEachChildNode_InvalidNode) { + /* forEachChildNodeCall with non-existent node — exercises the browse path. + * The server may return GOOD with empty results for unknown nodes. */ + UA_NodeId invalidNode = UA_NODEID_NUMERIC(99, 99999); + unsigned int prevCount = iteratedNodeCount; + UA_Client_forEachChildNodeCall(client, invalidNode, nodeIter, NULL); + /* No children should have been iterated */ + ck_assert_uint_eq(iteratedNodeCount, prevCount); +} +END_TEST + +START_TEST(Highlevel_WriteRead_Direct) { + /* UA_Client_write with NULL argument */ + UA_StatusCode retval = UA_Client_write(client, NULL); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINTERNALERROR); + + /* UA_Client_write with a valid write value */ + UA_WriteValue wv; + UA_WriteValue_init(&wv); + wv.nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME); + wv.attributeId = UA_ATTRIBUTEID_VALUE; + wv.value.hasValue = true; + UA_DateTime now = UA_DateTime_now(); + UA_Variant_setScalar(&wv.value.value, &now, &UA_TYPES[UA_TYPES_DATETIME]); + retval = UA_Client_write(client, &wv); + /* Result may be BADWRITENOTSUPPORTED for read-only node, but path is hit */ + (void)retval; + + /* UA_Client_read with a valid read value id */ + UA_ReadValueId rv; + UA_ReadValueId_init(&rv); + rv.nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME); + rv.attributeId = UA_ATTRIBUTEID_VALUE; + UA_DataValue dval = UA_Client_read(client, &rv); + ck_assert_uint_eq(dval.status, UA_STATUSCODE_GOOD); + ck_assert(dval.hasValue); + UA_DataValue_clear(&dval); +} +END_TEST + +START_TEST(Highlevel_BrowseSimplified) { + /* Test UA_Client_browse highlevel wrapper */ + UA_BrowseDescription bd; + UA_BrowseDescription_init(&bd); + bd.nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER); + bd.resultMask = UA_BROWSERESULTMASK_ALL; + + UA_BrowseResult br = UA_Client_browse(client, NULL, 2, &bd); + ck_assert_uint_eq(br.statusCode, UA_STATUSCODE_GOOD); + ck_assert(br.referencesSize > 0); + + /* Use browseNext if there's a continuation point */ + if(br.continuationPoint.length > 0) { + UA_BrowseResult br2 = + UA_Client_browseNext(client, false, br.continuationPoint); + ck_assert_uint_eq(br2.statusCode, UA_STATUSCODE_GOOD); + + /* Release continuation point */ + if(br2.continuationPoint.length > 0) { + UA_BrowseResult br3 = + UA_Client_browseNext(client, true, br2.continuationPoint); + UA_BrowseResult_clear(&br3); + } + UA_BrowseResult_clear(&br2); + } + + UA_BrowseResult_clear(&br); +} +END_TEST + +#ifdef UA_ENABLE_METHODCALLS +START_TEST(Highlevel_CallMethod_Error) { + /* Call a method on a non-existent node */ + size_t outputSize = 0; + UA_Variant *output = NULL; + UA_StatusCode retval = UA_Client_call(client, + UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), + UA_NODEID_NUMERIC(1, 99999), + 0, NULL, &outputSize, &output); + ck_assert(retval != UA_STATUSCODE_GOOD); + ck_assert_uint_eq(outputSize, 0); + ck_assert(output == NULL); +} +END_TEST +#endif + static Suite *testSuite_Client(void) { Suite *s = suite_create("Client Highlevel"); TCase *tc_misc = tcase_create("Client Highlevel Misc"); @@ -1081,6 +1263,23 @@ static Suite *testSuite_Client(void) { tcase_add_test(tc_readwrite, Node_ReadWrite_UserExecutable); suite_add_tcase(s, tc_readwrite); #endif + + TCase *tc_ext = tcase_create("Client Highlevel Extended"); + tcase_add_checked_fixture(tc_ext, setup, teardown); + tcase_add_test(tc_ext, Highlevel_TranslateBrowsePath); + tcase_add_test(tc_ext, Highlevel_WriteValueScalar); + tcase_add_test(tc_ext, Highlevel_WriteValueEx); +#ifdef UA_ENABLE_NODEMANAGEMENT + tcase_add_test(tc_ext, Highlevel_ReadWriteAccessLevelEx); +#endif + tcase_add_test(tc_ext, Highlevel_ForEachChildNode_InvalidNode); + tcase_add_test(tc_ext, Highlevel_WriteRead_Direct); + tcase_add_test(tc_ext, Highlevel_BrowseSimplified); +#ifdef UA_ENABLE_METHODCALLS + tcase_add_test(tc_ext, Highlevel_CallMethod_Error); +#endif + suite_add_tcase(s, tc_ext); + return s; } diff --git a/tests/client/check_client_subscriptions.c b/tests/client/check_client_subscriptions.c index e00f5e24e..e5b524f5d 100644 --- a/tests/client/check_client_subscriptions.c +++ b/tests/client/check_client_subscriptions.c @@ -1597,6 +1597,339 @@ START_TEST(Client_subscription_transfer) { } END_TEST +START_TEST(Client_subscription_getSetContext) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Get context — initially NULL */ + void *ctx = (void*)0xDEAD; + retval = UA_Client_Subscriptions_getContext(client, subId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_ptr_eq(ctx, NULL); + + /* Set context */ + int myData = 42; + retval = UA_Client_Subscriptions_setContext(client, subId, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Get context again */ + retval = UA_Client_Subscriptions_getContext(client, subId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_ptr_eq(ctx, &myData); + + /* Invalid subscription id */ + retval = UA_Client_Subscriptions_getContext(client, 99999, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADSUBSCRIPTIONIDINVALID); + + retval = UA_Client_Subscriptions_setContext(client, 99999, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADSUBSCRIPTIONIDINVALID); + + /* NULL args */ + retval = UA_Client_Subscriptions_getContext(client, subId, NULL); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + retval = UA_Client_Subscriptions_getContext(NULL, subId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_subscription_deleteSingle) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Delete it */ + retval = UA_Client_Subscriptions_deleteSingle(client, subId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Delete again — should fail */ + retval = UA_Client_Subscriptions_deleteSingle(client, subId); + ck_assert(retval != UA_STATUSCODE_GOOD); + + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_subscription_setPublishingMode) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Disable publishing */ + UA_SetPublishingModeRequest spmReq; + UA_SetPublishingModeRequest_init(&spmReq); + spmReq.publishingEnabled = false; + spmReq.subscriptionIds = &subId; + spmReq.subscriptionIdsSize = 1; + UA_SetPublishingModeResponse spmResp = + UA_Client_Subscriptions_setPublishingMode(client, spmReq); + ck_assert_uint_eq(spmResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + ck_assert_uint_eq(spmResp.resultsSize, 1); + ck_assert_uint_eq(spmResp.results[0], UA_STATUSCODE_GOOD); + UA_SetPublishingModeResponse_clear(&spmResp); + + /* Re-enable publishing */ + spmReq.publishingEnabled = true; + spmResp = UA_Client_Subscriptions_setPublishingMode(client, spmReq); + ck_assert_uint_eq(spmResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_SetPublishingModeResponse_clear(&spmResp); + + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +static void +dataChangeCallback_ext(UA_Client *c, UA_UInt32 subId, void *subContext, + UA_UInt32 monId, void *monContext, + UA_DataValue *value) { + /* no-op: just need a valid callback */ +} + +START_TEST(Client_monitoredItem_getSetContext) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Create a monitored item */ + UA_MonitoredItemCreateRequest monReq = + UA_MonitoredItemCreateRequest_default( + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME)); + UA_MonitoredItemCreateResult monResult = + UA_Client_MonitoredItems_createDataChange(client, subId, + UA_TIMESTAMPSTORETURN_BOTH, monReq, NULL, dataChangeCallback_ext, NULL); + ck_assert_uint_eq(monResult.statusCode, UA_STATUSCODE_GOOD); + UA_UInt32 monId = monResult.monitoredItemId; + + /* Get context — initially NULL */ + void *ctx = (void*)0xDEAD; + retval = UA_Client_MonitoredItem_getContext(client, subId, monId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_ptr_eq(ctx, NULL); + + /* Set context */ + int myData = 123; + retval = UA_Client_MonitoredItem_setContext(client, subId, monId, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Get context again */ + retval = UA_Client_MonitoredItem_getContext(client, subId, monId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert_ptr_eq(ctx, &myData); + + /* Invalid subscription id */ + retval = UA_Client_MonitoredItem_getContext(client, 99999, monId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADSUBSCRIPTIONIDINVALID); + + /* Invalid monitored item id */ + retval = UA_Client_MonitoredItem_getContext(client, subId, 99999, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADMONITOREDITEMIDINVALID); + + /* NULL args */ + retval = UA_Client_MonitoredItem_getContext(client, subId, monId, NULL); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + retval = UA_Client_MonitoredItem_getContext(NULL, subId, monId, &ctx); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + /* Set context with invalid sub */ + retval = UA_Client_MonitoredItem_setContext(client, 99999, monId, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADSUBSCRIPTIONIDINVALID); + + retval = UA_Client_MonitoredItem_setContext(client, subId, 99999, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADMONITOREDITEMIDINVALID); + + retval = UA_Client_MonitoredItem_setContext(NULL, subId, monId, &myData); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + UA_Client_MonitoredItems_deleteSingle(client, subId, monId); + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_subscription_setMonitoringMode) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription + monitored item */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + UA_MonitoredItemCreateRequest monReq = + UA_MonitoredItemCreateRequest_default( + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME)); + UA_MonitoredItemCreateResult monResult = + UA_Client_MonitoredItems_createDataChange(client, subId, + UA_TIMESTAMPSTORETURN_BOTH, monReq, NULL, dataChangeCallback_ext, NULL); + ck_assert_uint_eq(monResult.statusCode, UA_STATUSCODE_GOOD); + UA_UInt32 monId = monResult.monitoredItemId; + + /* Set monitoring mode to Disabled */ + UA_SetMonitoringModeRequest smmReq; + UA_SetMonitoringModeRequest_init(&smmReq); + smmReq.subscriptionId = subId; + smmReq.monitoringMode = UA_MONITORINGMODE_DISABLED; + smmReq.monitoredItemIds = &monId; + smmReq.monitoredItemIdsSize = 1; + UA_SetMonitoringModeResponse smmResp = + UA_Client_MonitoredItems_setMonitoringMode(client, smmReq); + ck_assert_uint_eq(smmResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_SetMonitoringModeResponse_clear(&smmResp); + + /* Set back to Reporting */ + smmReq.monitoringMode = UA_MONITORINGMODE_REPORTING; + smmResp = UA_Client_MonitoredItems_setMonitoringMode(client, smmReq); + ck_assert_uint_eq(smmResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_SetMonitoringModeResponse_clear(&smmResp); + + UA_Client_MonitoredItems_deleteSingle(client, subId, monId); + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_subscription_setTriggering) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Create trigger and triggering items */ + UA_MonitoredItemCreateRequest monReqs[2]; + monReqs[0] = UA_MonitoredItemCreateRequest_default( + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME)); + monReqs[1] = UA_MonitoredItemCreateRequest_default( + UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE)); + + UA_MonitoredItemCreateResult monResults[2]; + monResults[0] = UA_Client_MonitoredItems_createDataChange(client, subId, + UA_TIMESTAMPSTORETURN_BOTH, monReqs[0], NULL, dataChangeCallback_ext, NULL); + ck_assert_uint_eq(monResults[0].statusCode, UA_STATUSCODE_GOOD); + monResults[1] = UA_Client_MonitoredItems_createDataChange(client, subId, + UA_TIMESTAMPSTORETURN_BOTH, monReqs[1], NULL, dataChangeCallback_ext, NULL); + ck_assert_uint_eq(monResults[1].statusCode, UA_STATUSCODE_GOOD); + + UA_UInt32 triggeringItemId = monResults[0].monitoredItemId; + UA_UInt32 linkedItemId = monResults[1].monitoredItemId; + + /* SetTriggering: add a link */ + UA_SetTriggeringRequest stReq; + UA_SetTriggeringRequest_init(&stReq); + stReq.subscriptionId = subId; + stReq.triggeringItemId = triggeringItemId; + stReq.linksToAdd = &linkedItemId; + stReq.linksToAddSize = 1; + UA_SetTriggeringResponse stResp = + UA_Client_MonitoredItems_setTriggering(client, stReq); + ck_assert_uint_eq(stResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_SetTriggeringResponse_clear(&stResp); + + /* Remove the link */ + UA_SetTriggeringRequest stReq2; + UA_SetTriggeringRequest_init(&stReq2); + stReq2.subscriptionId = subId; + stReq2.triggeringItemId = triggeringItemId; + stReq2.linksToRemove = &linkedItemId; + stReq2.linksToRemoveSize = 1; + stResp = UA_Client_MonitoredItems_setTriggering(client, stReq2); + ck_assert_uint_eq(stResp.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_SetTriggeringResponse_clear(&stResp); + + UA_Client_MonitoredItems_deleteSingle(client, subId, monResults[0].monitoredItemId); + UA_Client_MonitoredItems_deleteSingle(client, subId, monResults[1].monitoredItemId); + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + +START_TEST(Client_subscription_modifyAsync) { + UA_Client *client = UA_Client_newForUnitTest(); + UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840"); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Create subscription */ + UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default(); + UA_CreateSubscriptionResponse response = + UA_Client_Subscriptions_create(client, request, NULL, NULL, NULL); + ck_assert_uint_eq(response.responseHeader.serviceResult, UA_STATUSCODE_GOOD); + UA_UInt32 subId = response.subscriptionId; + + /* Modify via async */ + UA_ModifySubscriptionRequest modReq; + UA_ModifySubscriptionRequest_init(&modReq); + modReq.subscriptionId = subId; + modReq.requestedPublishingInterval = 200.0; + modReq.requestedMaxKeepAliveCount = 20; + modReq.requestedLifetimeCount = 600; + modReq.maxNotificationsPerPublish = 100; + + UA_UInt32 reqId = 0; + retval = UA_Client_Subscriptions_modify_async(client, modReq, + NULL, NULL, &reqId); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + ck_assert(reqId != 0); + + /* Drive iterations to get response */ + for(int i = 0; i < 20; i++) { + UA_fakeSleep(10); + UA_Client_run_iterate(client, 1); + } + + UA_Client_Subscriptions_deleteSingle(client, subId); + UA_Client_disconnect(client); + UA_Client_delete(client); +} +END_TEST + #ifdef UA_ENABLE_METHODCALLS START_TEST(Client_methodcall) { UA_Client *client = UA_Client_newForUnitTest(); @@ -1683,6 +2016,13 @@ static Suite* testSuite_Client(void) { tcase_add_test(tc_client, Client_subscription_server_disappears); tcase_add_test(tc_client, Client_subscription_transfer); tcase_add_test(tc_client, Client_subscription_writeBurst); + tcase_add_test(tc_client, Client_subscription_getSetContext); + tcase_add_test(tc_client, Client_subscription_deleteSingle); + tcase_add_test(tc_client, Client_subscription_setPublishingMode); + tcase_add_test(tc_client, Client_monitoredItem_getSetContext); + tcase_add_test(tc_client, Client_subscription_setMonitoringMode); + tcase_add_test(tc_client, Client_subscription_setTriggering); + tcase_add_test(tc_client, Client_subscription_modifyAsync); suite_add_tcase(s,tc_client); #ifdef UA_ENABLE_METHODCALLS diff --git a/tests/client/check_client_subscriptions_datachange.c b/tests/client/check_client_subscriptions_datachange.c index e1825d6c2..66909f99d 100644 --- a/tests/client/check_client_subscriptions_datachange.c +++ b/tests/client/check_client_subscriptions_datachange.c @@ -269,9 +269,6 @@ START_TEST(sub_data_change_notification) { break; } - /* The subscription create and monitored item create are the main coverage - * hits. Whether the notification arrives depends on timing. */ - /* Write a new value */ UA_Int32 newVal = 777; UA_Variant wv; @@ -561,6 +558,64 @@ START_TEST(client_register_nodes) { disconnectClient(client); } END_TEST +/* === Client getRemoteDataTypes (covers ua_client_util.c) === */ + +START_TEST(client_getRemoteDataTypes_browse) { + /* Test with dataTypesNodesSize == 0, which triggers the browse-based + * discovery of unknown DataTypes in the server's type hierarchy */ + UA_Client *client = connectClient(); + + UA_DataTypeArray *customTypes = NULL; + UA_StatusCode retval = + UA_Client_getRemoteDataTypes(client, 0, NULL, &customTypes); + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + /* Add to client config so it gets cleaned up on client delete */ + if(customTypes) { + UA_ClientConfig *cc = UA_Client_getConfig(client); + customTypes->next = cc->customDataTypes; + cc->customDataTypes = customTypes; + } + + disconnectClient(client); +} +END_TEST + +START_TEST(client_getRemoteDataTypes_explicit) { + /* Test with explicit NodeIds — use a known DataType node e.g. Range */ + UA_Client *client = connectClient(); + + UA_NodeId nodes[1]; + nodes[0] = UA_NODEID_NUMERIC(0, UA_NS0ID_RANGE); + + UA_DataTypeArray *customTypes = NULL; + UA_StatusCode retval = + UA_Client_getRemoteDataTypes(client, 1, nodes, &customTypes); + /* Range is already known, so the result array may be empty but call succeeds */ + ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD); + + if(customTypes) { + UA_ClientConfig *cc = UA_Client_getConfig(client); + customTypes->next = cc->customDataTypes; + cc->customDataTypes = customTypes; + } + + disconnectClient(client); +} +END_TEST + +START_TEST(client_getRemoteDataTypes_nullArg) { + /* NULL customTypes argument => BADINVALIDARGUMENT */ + UA_Client *client = connectClient(); + + UA_StatusCode retval = + UA_Client_getRemoteDataTypes(client, 0, NULL, NULL); + ck_assert_uint_eq(retval, UA_STATUSCODE_BADINVALIDARGUMENT); + + disconnectClient(client); +} +END_TEST + /* === Suite definition === */ static Suite *testSuite_clientSubExt2(void) { TCase *tc_sub = tcase_create("SubLifecycle"); @@ -589,6 +644,15 @@ static Suite *testSuite_clientSubExt2(void) { suite_add_tcase(s, tc_sub); suite_add_tcase(s, tc_disc); suite_add_tcase(s, tc_ops); + + TCase *tc_util = tcase_create("ClientUtil"); + tcase_add_checked_fixture(tc_util, setup, teardown); + tcase_set_timeout(tc_util, 30); + tcase_add_test(tc_util, client_getRemoteDataTypes_browse); + tcase_add_test(tc_util, client_getRemoteDataTypes_explicit); + tcase_add_test(tc_util, client_getRemoteDataTypes_nullArg); + suite_add_tcase(s, tc_util); + return s; }