/* 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 2026 (c) o6 Automation GmbH (Author: Andreas Ebner) * * Coverage tests for src/server/ua_services_method.c error paths that * the existing 14-test check_services_call.c suite does not exercise: * * - getArgumentsNodeCallback: the "found" branch that returns a * variable node reference. The existing tests use methods * WITHOUT defined InputArguments, so the callback is never * invoked successfully. * - checkAdjustArguments: the type-mismatch branch that returns * BADINVALIDARGUMENT. The existing tests use a method without * input arguments and so skip the type check entirely. * - The "args mismatch" early returns in callWithResolvedMethodAndObject. * * Each test adds its own method to a fresh sub-Object so the previous * test's argument definitions cannot leak across. */ #include #include #include #include "server/ua_server_internal.h" #include "server/ua_services.h" #include "test_helpers.h" #include #include #include static UA_Server *server; static UA_NodeId testObjectId; static UA_NodeId methodWithInt32In; static UA_NodeId methodNoArgs; static UA_NodeId methodWithOutput; static UA_NodeId outputArgumentsId; static UA_UInt32 lastMethodCalled = 0; static size_t lastOutputSize = 0; static UA_StatusCode testMethodCallback(UA_Server *s, const UA_NodeId *sessionId, void *sessionHandle, const UA_NodeId *methodId, void *methodContext, const UA_NodeId *objectId, void *objectContext, size_t inputSize, const UA_Variant *input, size_t outputSize, UA_Variant *output) { (void)s; (void)sessionId; (void)sessionHandle; (void)methodId; (void)methodContext; (void)objectId; (void)objectContext; (void)input; (void)output; (void)inputSize; lastMethodCalled = 1; lastOutputSize = outputSize; return UA_STATUSCODE_GOOD; } static void setup(void) { server = UA_Server_newForUnitTest(); ck_assert_ptr_ne(server, NULL); UA_Server_run_startup(server); lastMethodCalled = 0; lastOutputSize = 0; /* Create a parent Object that will own our test methods. */ UA_ObjectAttributes objAttr = UA_ObjectAttributes_default; objAttr.displayName = UA_LOCALIZEDTEXT("en-US", "MethodTestObj"); UA_NodeId objId; UA_StatusCode res = UA_Server_addObjectNode( server, UA_NODEID_NULL, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), UA_QUALIFIEDNAME(1, "MethodTestObj"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), objAttr, NULL, &objId); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); UA_NodeId_copy(&objId, &testObjectId); /* Method #1: takes one Int32 input argument. */ UA_Argument inArg; UA_Argument_init(&inArg); inArg.name = UA_STRING("x"); inArg.dataType = UA_TYPES[UA_TYPES_INT32].typeId; inArg.valueRank = -1; /* scalar */ UA_MethodAttributes methAttr = UA_MethodAttributes_default; methAttr.displayName = UA_LOCALIZEDTEXT("en-US", "methodWithInt32In"); methAttr.executable = true; res = UA_Server_addMethodNode( server, UA_NODEID_NULL, objId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "methodWithInt32In"), methAttr, testMethodCallback, 1, &inArg, 0, NULL, NULL, &methodWithInt32In); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); /* Method #2: takes no arguments. */ methAttr.displayName = UA_LOCALIZEDTEXT("en-US", "methodNoArgs"); res = UA_Server_addMethodNode( server, UA_NODEID_NULL, objId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "methodNoArgs"), methAttr, testMethodCallback, 0, NULL, 0, NULL, NULL, &methodNoArgs); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); /* Method #3: returns one String output argument. Keep the NodeId of the * OutputArguments property so the tests can replace its value source. */ UA_Argument outArg; UA_Argument_init(&outArg); outArg.name = UA_STRING("result"); outArg.dataType = UA_TYPES[UA_TYPES_STRING].typeId; outArg.valueRank = UA_VALUERANK_SCALAR; methAttr.displayName = UA_LOCALIZEDTEXT("en-US", "methodWithOutput"); res = UA_Server_addMethodNodeEx( server, UA_NODEID_NULL, objId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT), UA_QUALIFIEDNAME(1, "methodWithOutput"), methAttr, testMethodCallback, 0, NULL, UA_NODEID_NULL, NULL, 1, &outArg, UA_NODEID_NULL, &outputArgumentsId, NULL, &methodWithOutput); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); } static void teardown(void) { UA_NodeId_clear(&testObjectId); UA_NodeId_clear(&methodWithInt32In); UA_NodeId_clear(&methodNoArgs); UA_NodeId_clear(&methodWithOutput); UA_NodeId_clear(&outputArgumentsId); UA_Server_run_shutdown(server); UA_Server_delete(server); } /* ==== Tests ==== */ /* Calling a no-argument method with arguments must return * BADTOOMANYARGUMENTS. This path is hit in callWithResolvedMethodAndObject * (line 181 of ua_services_method.c in the source tree). */ START_TEST(Call_noArgsMethod_withArgs_rejected) { UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodNoArgs; /* Inject one bogus argument. */ UA_Variant bogus; UA_Variant_init(&bogus); UA_Int32 v = 42; UA_Variant_setScalarCopy(&bogus, &v, &UA_TYPES[UA_TYPES_INT32]); req.inputArgumentsSize = 1; req.inputArguments = &bogus; UA_CallMethodResult result; UA_CallMethodResult_init(&result); result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_BADTOOMANYARGUMENTS); /* Method callback must not have run. */ ck_assert_uint_eq(lastMethodCalled, 0); UA_CallMethodResult_clear(&result); UA_Variant_clear(&bogus); } END_TEST /* Calling a method with fewer arguments than its definition requires * returns BADARGUMENTSMISSING. The arguments are size-checked before * the type-check so a wrong count is reported first. */ START_TEST(Call_method_missingArgs_rejected) { UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithInt32In; /* No input arguments supplied. */ UA_CallMethodResult result; UA_CallMethodResult_init(&result); result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_BADARGUMENTSMISSING); ck_assert_uint_eq(lastMethodCalled, 0); UA_CallMethodResult_clear(&result); } END_TEST /* Calling a method with the right number of arguments but the wrong * type must return BADINVALIDARGUMENT. This exercises * checkAdjustArguments: getArgumentsVariableNode finds the input- * argument description, the type comparison fails, and the per- * argument inputArgumentResults entry is filled with * BADTYPEMISMATCH. */ START_TEST(Call_method_typeMismatch_rejected) { UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithInt32In; /* The method wants an Int32 but we send a String. */ UA_Variant wrongType; UA_Variant_init(&wrongType); UA_String s = UA_STRING("not an int"); UA_Variant_setScalarCopy(&wrongType, &s, &UA_TYPES[UA_TYPES_STRING]); req.inputArgumentsSize = 1; req.inputArguments = &wrongType; UA_CallMethodResult result; UA_CallMethodResult_init(&result); result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_BADINVALIDARGUMENT); /* The per-argument result array is set so the caller can see * exactly which argument failed. */ ck_assert_uint_eq(result.inputArgumentResultsSize, 1); ck_assert(result.inputArgumentResults != NULL); ck_assert_uint_eq(result.inputArgumentResults[0], UA_STATUSCODE_BADTYPEMISMATCH); ck_assert_uint_eq(lastMethodCalled, 0); UA_CallMethodResult_clear(&result); UA_Variant_clear(&wrongType); } END_TEST /* Calling a method with the right number and type of arguments must * succeed and invoke the callback. This is the success path of * getArgumentsNodeCallback + checkAdjustArguments + the actual * method dispatch. */ START_TEST(Call_method_correctArgs_succeeds) { UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithInt32In; UA_Variant correctArg; UA_Variant_init(&correctArg); UA_Int32 v = 7; UA_Variant_setScalarCopy(&correctArg, &v, &UA_TYPES[UA_TYPES_INT32]); req.inputArgumentsSize = 1; req.inputArguments = &correctArg; UA_CallMethodResult result; UA_CallMethodResult_init(&result); result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_GOOD); ck_assert_uint_eq(lastMethodCalled, 1); UA_CallMethodResult_clear(&result); UA_Variant_clear(&correctArg); } END_TEST START_TEST(Call_nonInternalOutputArguments_rejected) { UA_CallbackValueSource source = {NULL, NULL}; UA_StatusCode res = UA_Server_setVariableNode_callbackValueSource( server, outputArgumentsId, source); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithOutput; UA_CallMethodResult result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_BADINTERNALERROR); ck_assert_uint_eq(lastMethodCalled, 0); UA_CallMethodResult_clear(&result); } END_TEST START_TEST(Call_scalarOutputArgument_countsAsOne) { UA_Argument outArg; UA_Argument_init(&outArg); outArg.name = UA_STRING("scalar-result"); outArg.dataType = UA_TYPES[UA_TYPES_STRING].typeId; outArg.valueRank = UA_VALUERANK_SCALAR; UA_DataValue value; UA_DataValue_init(&value); value.hasValue = true; UA_Variant_setScalar(&value.value, &outArg, &UA_TYPES[UA_TYPES_ARGUMENT]); value.value.storageType = UA_VARIANT_DATA_NODELETE; UA_StatusCode res = UA_Server_setVariableNode_internalValueSource( server, outputArgumentsId, &value, NULL); ck_assert_uint_eq(res, UA_STATUSCODE_GOOD); UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithOutput; UA_CallMethodResult result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_GOOD); ck_assert_uint_eq(lastMethodCalled, 1); ck_assert_uint_eq(lastOutputSize, 1); ck_assert_uint_eq(result.outputArgumentsSize, 1); UA_CallMethodResult_clear(&result); } END_TEST START_TEST(Check_methodOutputArgumentCount) { ck_assert_uint_eq(checkMethodOutputArguments(0, 1), UA_STATUSCODE_BADARGUMENTSMISSING); ck_assert_uint_eq(checkMethodOutputArguments(1, 1), UA_STATUSCODE_GOOD); ck_assert_uint_eq(checkMethodOutputArguments(2, 1), UA_STATUSCODE_BADTOOMANYARGUMENTS); } END_TEST /* ==== UA_MAX_METHOD_ARGUMENTS guard ==== */ START_TEST(Call_method_exceedsMaxArgs_rejected) { /* src/server/ua_services_method.c:181-182: * if(request->inputArgumentsSize > UA_MAX_METHOD_ARGUMENTS) * return UA_STATUSCODE_BADTOOMANYARGUMENTS; * UA_MAX_METHOD_ARGUMENTS is 64 (defined in ua_services_method.c, * not exposed in a public header). The existing * Call_method_typeMismatch_rejected / Call_method_missingArgs_rejected * use 0-1 args; the >64 branch is not exercised. */ UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = methodWithInt32In; /* 65 args exceeds UA_MAX_METHOD_ARGUMENTS (= 64). */ const size_t size = 65; UA_Variant args[65]; memset(args, 0, sizeof(args)); for(size_t i = 0; i < size; i++) { UA_Variant_init(&args[i]); } req.inputArgumentsSize = size; req.inputArguments = args; UA_CallMethodResult result = UA_Server_call(server, &req); ck_assert_uint_eq(result.statusCode, UA_STATUSCODE_BADTOOMANYARGUMENTS); UA_CallMethodResult_clear(&result); for(size_t i = 0; i < size; i++) UA_Variant_clear(&args[i]); } END_TEST /* ==== Method node with wrong NodeClass ==== */ START_TEST(Call_methodNode_wrongNodeClass_rejected) { /* src/server/ua_services_method.c: ~270-271 (Operation_CallMethod): * The method's browseName is fetched via lookup; if the resulting * node is not UA_NODECLASS_METHOD, the call is rejected. The * existing tests use a valid methodId (the method is a * UA_NODECLASS_METHOD node) -- they don't exercise the * 'methodId points to a non-method node' branch. * We use one of the object nodes we created earlier as a * bogus methodId. */ UA_CallMethodRequest req; UA_CallMethodRequest_init(&req); req.objectId = testObjectId; req.methodId = testObjectId; /* not a method node */ UA_CallMethodResult result = UA_Server_call(server, &req); ck_assert(result.statusCode != UA_STATUSCODE_GOOD); UA_CallMethodResult_clear(&result); } END_TEST /* ==== Suite ==== */ static Suite * testSuite(void) { Suite *s = suite_create("Services Method Coverage"); TCase *tc = tcase_create("Operation_CallMethod error paths"); tcase_set_timeout(tc, 30); tcase_add_checked_fixture(tc, setup, teardown); tcase_add_test(tc, Call_noArgsMethod_withArgs_rejected); tcase_add_test(tc, Call_method_missingArgs_rejected); tcase_add_test(tc, Call_method_typeMismatch_rejected); tcase_add_test(tc, Call_method_correctArgs_succeeds); tcase_add_test(tc, Call_nonInternalOutputArguments_rejected); tcase_add_test(tc, Call_scalarOutputArgument_countsAsOne); tcase_add_test(tc, Check_methodOutputArgumentCount); tcase_add_test(tc, Call_method_exceedsMaxArgs_rejected); tcase_add_test(tc, Call_methodNode_wrongNodeClass_rejected); suite_add_tcase(s, tc); return s; } int main(void) { Suite *s = testSuite(); 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); return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; }