diff --git a/README.md b/README.md index eb0cfcaea..1f4f42556 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,26 @@ make sudo make install ``` +### Optional Build Features + +| Flag | Macro | Default | Description | +|------|-------|---------|-------------| +| `--enable-dynamic-table-support` | `ENABLE_DYNAMIC_TABLE_SUPPORT` | Disabled | Dynamic TR-181 table traversal via `dataModelTable` profile entries | + +#### Dynamic Table Support + +When enabled (`./configure --enable-dynamic-table-support=yes`), this unlocks: + +- **`dataModelTable` parameter type** in report profiles — collect structured multi-row data from any TR-181 table (e.g., `Device.WiFi.Radio.`, `Device.DHCPv6.Server.Pool.`) +- **Index-based row selection** — filter specific rows using comma-separated indices or ranges (e.g., `"1,2"`, `"1-4"`) +- **Wildcard collection** — omit index to collect all available table rows automatically +- **Nested table traversal** — support sub-tables within tables +- **Structured JSON array encoding** — report output keyed by table base path with 1-based row positioning + +When disabled (default), profiles containing `dataModelTable` entries are silently ignored at runtime with no impact on existing functionality. + +See [Profile Schema](schemas/t2_reportProfileSchema.schema.json) for the `dataModelTable` JSON schema definition. + ### Docker Development Refer to the provided Docker container for a consistent development environment: diff --git a/build_inside_container.sh b/build_inside_container.sh index f962ea152..4f0cc3c83 100755 --- a/build_inside_container.sh +++ b/build_inside_container.sh @@ -31,4 +31,4 @@ export CFLAGS=" ${DEBUG_CFLAGS} -I${INSTALL_DIR}/include/rtmessage -I${INSTALL_D export LDFLAGS="-L/usr/lib/x86_64-linux-gnu -lglib-2.0" -./configure --prefix=${INSTALL_DIR} --enable-rdkcertselector=yes && make && make install +./configure --prefix=${INSTALL_DIR} --enable-rdkcertselector=yes --enable-dynamic-table-support=yes && make && make install diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 539678387..d6545359a 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -113,6 +113,7 @@ graph TB - **Purpose**: Retrieve TR-181 data model parameters - **Protocol**: D-Bus (CCSP) or RBUS - **Caching**: Parameter value cache with TTL +- **Dynamic Tables**: Supports structured traversal of multi-instance TR-181 objects via `dataModelTable` profile entries - **Files**: `source/ccspinterface/` ### 3. Processing Layer diff --git a/schemas/t2_reportProfileSchema.schema.json b/schemas/t2_reportProfileSchema.schema.json index 1b5733fc1..a47c4d45b 100644 --- a/schemas/t2_reportProfileSchema.schema.json +++ b/schemas/t2_reportProfileSchema.schema.json @@ -98,31 +98,34 @@ }, "dataModelSimple": { "title": "\"dataModel\" Parameter (restricted for use in dataModelTable)", + "description": "A simplified dataModel parameter used within dataModelTable entries. The 'reference' is a leaf parameter name relative to the table row path (e.g. 'Enable', 'SSID', 'Channel').", "type": "object", "properties": { "type": { "type": "string", "const": "dataModel" }, - "reference": { "type": "string" } + "reference": { "type": "string", "description": "Leaf parameter name relative to the table row (e.g. 'Enable', 'Status', 'SSID')." } }, "required": ["type", "reference"], "additionalProperties": false }, "dataModelTable": { "title": "\"dataModelTable\" Parameter", + "description": "A dataModelTable parameter enables structured traversal of TR-181 multi-instance objects (tables). It collects sub-parameter values for each row in the table and encodes them as a JSON array keyed by the table base path.", "type": "object", "properties": { - "type": { "type": "string", "const": "dataModelTable" }, + "type": { "type": "string", "const": "dataModelTable", "description": "Defines a dynamic table parameter for multi-row TR-181 data collection." }, "reference": { "type": "string", "pattern": ".*\\.$", - "description": "Reference must end with a dot '.'" + "description": "The TR-181 table base path. Must end with a dot '.', e.g. 'Device.WiFi.Radio.' or 'Device.DHCPv6.Server.Pool.'" }, "index": { "type": "string", "pattern": "^(\\d+(-\\d+)?)(,(\\d+(-\\d+)?))*$", - "description": "Comma separated numbers, ranges (e.g. 1-4), or combinations (e.g. 1,2,5-7)." + "description": "Optional. Comma-separated row indices or ranges to collect. Examples: '1' (single row), '1-4' (range), '1,3,5' (list), '1-2,5,7-9' (mixed). Values must be 0-255. If omitted, all available rows are collected via wildcard query." }, "Parameter": { "type": "array", + "description": "Array of sub-parameters to collect from each table row. Each item must be a dataModel (leaf parameter) or a nested dataModelTable (sub-table traversal).", "items": { "oneOf": [ { "$ref": "#/definitions/parmDefinitions/properties/dataModelSimple" }, @@ -132,7 +135,8 @@ } }, "required": ["type", "reference", "Parameter"], - "additionalProperties": false + "additionalProperties": false, + "example": "{ \"type\": \"dataModelTable\", \"reference\": \"Device.WiFi.Radio.\", \"index\": \"1,2\", \"Parameter\": [ { \"type\": \"dataModel\", \"reference\": \"Enable\" }, { \"type\": \"dataModel\", \"reference\": \"Channel\" } ] }" } } }, diff --git a/source/t2parser/t2parser.c b/source/t2parser/t2parser.c index cda541dbc..2b9ab58c0 100644 --- a/source/t2parser/t2parser.c +++ b/source/t2parser/t2parser.c @@ -1994,7 +1994,12 @@ int msgpack_strcmp(msgpack_object *obj, char *str) { return -1; } - return strncmp(str, obj->via.str.ptr, obj->via.str.size); + size_t len = strlen(str); + if (obj->via.str.size != len) + { + return (obj->via.str.size < len) ? -1 : 1; + } + return strncmp(str, obj->via.str.ptr, len); } void msgpack_print(msgpack_object *obj, char *obj_name) @@ -2278,6 +2283,170 @@ T2ERROR time_param_MsgPackReporting_Adjustments_valid_set(Profile *profile, msgp return T2ERROR_SUCCESS; } +#ifdef ENABLE_DYNAMIC_TABLE_SUPPORT +static T2ERROR parseDataModelTableParamsMsgpack(Profile* profile, msgpack_object* tableMap, const char* parentPath, DataModelTable* parentTable) +{ + T2Debug("%s ++in\n", __FUNCTION__); + if (!tableMap || !parentPath || !profile) + { + T2Error("Invalid input parameters\n"); + return T2ERROR_FAILURE; + } + + T2Debug("Processing msgpack table with parent path: %s\n", parentPath); + + msgpack_object *mpReference = msgpack_get_map_value(tableMap, "reference"); + msgpack_object *mpIndex = msgpack_get_map_value(tableMap, "index"); + msgpack_object *mpParameters = msgpack_get_map_value(tableMap, "Parameter"); + + if (!mpReference || !mpParameters) + { + T2Error("Incomplete dataModelTable configuration in msgpack\n"); + return T2ERROR_FAILURE; + } + + char *referenceStr = msgpack_strdup(mpReference); + if (!referenceStr) + { + T2Error("Failed to extract reference string from msgpack\n"); + return T2ERROR_FAILURE; + } + + DataModelTable* currentTable = NULL; + if (!parentTable) + { + currentTable = (DataModelTable*)malloc(sizeof(DataModelTable)); + if (!currentTable) + { + T2Error("Failed to allocate memory for DataModelTable\n"); + free(referenceStr); + return T2ERROR_FAILURE; + } + + currentTable->reference = referenceStr; + currentTable->index = mpIndex ? msgpack_strdup(mpIndex) : NULL; + if (mpIndex && !currentTable->index) + { + T2Error("Failed to allocate memory for DataModelTable index\n"); + free(currentTable->reference); + free(currentTable); + return T2ERROR_FAILURE; + } + Vector_Create(¤tTable->paramList); + + if (!profile->dataModelTableList) + { + Vector_Create(&profile->dataModelTableList); + } + Vector_PushBack(profile->dataModelTableList, currentTable); + } + else + { + currentTable = parentTable; + } + + // Build the current path including wildcard + char currentPath[MAX_PATH_LENGTH]; + if (buildFullPath(currentPath, parentPath, referenceStr) != 0) + { + T2Error("Failed to build current path\n"); + if (!parentTable) + { + // Remove the table we just pushed and free it (also frees referenceStr via table->reference) + Vector_RemoveItem(profile->dataModelTableList, currentTable, freeDataModelTable); + } + else + { + free(referenceStr); + } + return T2ERROR_FAILURE; + } + + char pathWithWildcard[MAX_PATH_LENGTH]; + if ((size_t)snprintf(pathWithWildcard, sizeof(pathWithWildcard), "%s*.", currentPath) >= sizeof(pathWithWildcard)) + { + T2Error("Path with wildcard exceeded buffer size\n"); + if (!parentTable) + { + Vector_RemoveItem(profile->dataModelTableList, currentTable, freeDataModelTable); + } + else + { + free(referenceStr); + } + return T2ERROR_FAILURE; + } + + if (parentTable) + { + free(referenceStr); + } + + // Process parameters + uint32_t paramCount = 0; + MSGPACK_GET_ARRAY_SIZE(mpParameters, paramCount); + for (uint32_t i = 0; i < paramCount; i++) + { + msgpack_object *paramItem = msgpack_get_array_element(mpParameters, i); + if (!paramItem) + { + continue; + } + + msgpack_object *mpType = msgpack_get_map_value(paramItem, "type"); + msgpack_object *mpParamRef = msgpack_get_map_value(paramItem, "reference"); + + if (!mpType || !mpParamRef) + { + continue; + } + + if (0 == msgpack_strcmp(mpType, "dataModelTable")) + { + // Recursive call for nested tables + parseDataModelTableParamsMsgpack(profile, paramItem, pathWithWildcard, currentTable); + } + else if (0 == msgpack_strcmp(mpType, "dataModel")) + { + // Create DataModelParam + DataModelParam* param = (DataModelParam*)malloc(sizeof(DataModelParam)); + if (!param) + { + continue; + } + + param->reference = msgpack_strdup(mpParamRef); + char fullPath[MAX_PATH_LENGTH]; + if (buildFullPath(fullPath, pathWithWildcard, param->reference) != 0) + { + T2Error("Failed to build full path for parameter\n"); + if (param->reference) + { + free(param->reference); + } + free(param); + continue; + } + param->name = strdup(fullPath); + if (!param->name) + { + T2Error("Failed to allocate memory for DataModelParam name\n"); + free(param->reference); + free(param); + continue; + } + param->reportEmpty = false; + + Vector_PushBack(currentTable->paramList, param); + T2Debug("Added parameter: %s\n", fullPath); + } + } + + T2Debug("%s ++out\n", __FUNCTION__); + return T2ERROR_SUCCESS; +} +#endif + T2ERROR addParameterMsgpack_marker_config(Profile* profile, msgpack_object* value_map) { if(profile == NULL || value_map == NULL) @@ -2514,8 +2683,170 @@ T2ERROR addParameterMsgpack_marker_config(Profile* profile, msgpack_object* valu else if(0 == msgpack_strcmp(Parameter_type_str, "dataModelTable")) { #ifdef ENABLE_DYNAMIC_TABLE_SUPPORT - T2Debug("MsgPack dataModelTable parsing is enabled only in JSON flow currently\n"); - T2Error("%s dataModelTable in MsgPack profile is not supported in current implementation\n", __FUNCTION__); + T2Debug("Processing dataModelTable in MsgPack profile\n"); + msgpack_object *mpBaseRef = msgpack_get_map_value(Parameter_array_map, "reference"); + if (mpBaseRef) + { + char basePath[256] = ""; + char *baseRefStr = msgpack_strdup(mpBaseRef); + if (baseRefStr) + { + strncpy(basePath, baseRefStr, sizeof(basePath) - 1); + basePath[sizeof(basePath) - 1] = '\0'; + free(baseRefStr); + } + if (basePath[0] == '\0') + { + T2Error("Failed to extract or empty base reference for dataModelTable\n"); + free(paramtype); + free(use); + if (regex != NULL) + { + free(regex); + } + continue; + } + T2Debug("Base path for msgpack data model table: %s\n", basePath); + + msgpack_object *mpIndex = msgpack_get_map_value(Parameter_array_map, "index"); + if (mpIndex) + { + char index[64] = ""; + char *indexStr = msgpack_strdup(mpIndex); + if (indexStr) + { + strncpy(index, indexStr, sizeof(index) - 1); + index[sizeof(index) - 1] = '\0'; + free(indexStr); + } + // Remove whitespace from index + int ii = 0, jj = 0; + while (index[ii]) + { + if (!(index[ii] == ' ' || index[ii] == '\t' || index[ii] == '\n' || + index[ii] == '\r' || index[ii] == '\v' || index[ii] == '\f')) + { + index[jj++] = index[ii]; + } + ii++; + } + index[jj] = '\0'; + + int duplicate[256] = {0}; + char *token = strtok(index, ","); + while (token != NULL) + { + int start, end; + if (sscanf(token, "%d-%d", &start, &end) == 2) + { + for (int k = start; k <= end; ++k) + { + if (k < 0 || k >= 256) + { + continue; + } + if (duplicate[k]) + { + continue; + } + duplicate[k] = 1; + T2Debug("Processing index : %d\n", k); + char basePathWithIndex[256]; + int written = snprintf(basePathWithIndex, sizeof(basePathWithIndex), "%s%d.", basePath, k); + if (written < 0 || (size_t)written >= sizeof(basePathWithIndex)) + { + T2Error("%s: snprintf truncated or failed while building path: '%s'\n", __FUNCTION__, basePathWithIndex); + } + ret = addParameter(profile, basePathWithIndex, basePathWithIndex, logfile, skipFrequency, firstSeekFromEOF, "dataModel", use, reportEmpty, rtformat, trim, regex); + if (ret != T2ERROR_SUCCESS) + { + T2Error("%s Error in adding parameter to profile %s\n", __FUNCTION__, basePathWithIndex); + } + } + } + else + { + int val = atoi(token); + if (val < 0 || val >= 256) + { + token = strtok(NULL, ","); + continue; + } + if (duplicate[val]) + { + token = strtok(NULL, ","); + continue; + } + duplicate[val] = 1; + T2Debug("Processing index : %d\n", val); + char basePathWithIndex[256]; + int written = snprintf(basePathWithIndex, sizeof(basePathWithIndex), "%s%d.", basePath, val); + if (written < 0 || (size_t)written >= sizeof(basePathWithIndex)) + { + T2Error("%s: snprintf truncated or failed while building path: '%s'\n", __FUNCTION__, basePathWithIndex); + } + ret = addParameter(profile, basePathWithIndex, basePathWithIndex, logfile, skipFrequency, firstSeekFromEOF, "dataModel", use, reportEmpty, rtformat, trim, regex); + if (ret != T2ERROR_SUCCESS) + { + T2Error("%s Error in adding parameter to profile %s\n", __FUNCTION__, basePathWithIndex); + } + } + token = strtok(NULL, ","); + } + } + else + { + // No index: use basePath directly as a dataModel parameter + content = strdup(basePath); + header = strdup(basePath); + if (!content || !header) + { + T2Error("Memory allocation failed for content/header\n"); + free(content); + free(header); + free(paramtype); + free(use); + if (regex != NULL) + { + free(regex); + } + continue; + } + free(paramtype); + paramtype = strdup("dataModel"); + if (!paramtype) + { + T2Error("Memory allocation failed for paramtype\n"); + free(content); + free(header); + free(use); + if (regex != NULL) + { + free(regex); + } + continue; + } + // Parse sub-parameters for dynamic table structure (no-index case) + T2ERROR tableRet2 = parseDataModelTableParamsMsgpack(profile, Parameter_array_map, basePath, NULL); + if (tableRet2 != T2ERROR_SUCCESS) + { + T2Error("Failed to parse msgpack data model table configuration\n"); + } + // Fall through to addParameter below + goto msgpack_add_param; + } + + // Parse sub-parameters for dynamic table structure + T2ERROR tableRet = parseDataModelTableParamsMsgpack(profile, Parameter_array_map, basePath, NULL); + if (tableRet != T2ERROR_SUCCESS) + { + T2Error("Failed to parse msgpack data model table configuration\n"); + } + } + else + { + T2Error("Missing reference in msgpack dataModelTable configuration\n"); + } #else T2Debug("Dynamic table support disabled, ignoring dataModelTable parameter\n"); #endif @@ -2539,6 +2870,9 @@ T2ERROR addParameterMsgpack_marker_config(Profile* profile, msgpack_object* valu continue; } +#ifdef ENABLE_DYNAMIC_TABLE_SUPPORT +msgpack_add_param: +#endif T2Debug("%s : reportTimestamp = %d\n", __FUNCTION__, rtformat); if(header != NULL && content != NULL) { diff --git a/test/functional-tests/tests/mock_table_provider.c b/test/functional-tests/tests/mock_table_provider.c new file mode 100644 index 000000000..ae5b9a7a8 --- /dev/null +++ b/test/functional-tests/tests/mock_table_provider.c @@ -0,0 +1,197 @@ +/* + * Mock rbus table provider for L2 testing of dataModelTable feature. + * Registers Device.WiFi.AccessPoint.{1,2,3}.{SSID,Status,Enable} as + * indexed table parameters accessible via rbus. + * + * Build: gcc -o mock_table_provider mock_table_provider.c \ + * -I/usr/local/include -I/usr/local/include/rbus \ + * -L/usr/local/lib -lrbus -lrbuscore -lrtMessage -lmsgpackc + */ + +#include +#include +#include +#include +#include +#include + +#define NUM_ROWS 3 +#define NUM_PARAMS_PER_ROW 3 +#define TOTAL_PARAMS (NUM_ROWS * NUM_PARAMS_PER_ROW) + +static rbusHandle_t handle; + +/* Base table path - uses custom namespace to avoid conflicts with tr69hostif */ +#define TABLE_BASE "Device.X_T2TEST_Table.AccessPoint." + +/* Table data */ +static const char *ssid_values[NUM_ROWS] = {"HomeNetwork", "GuestNetwork", "IoT_Network"}; +static const char *status_values[NUM_ROWS] = {"Enabled", "Enabled", "Disabled"}; +static const char *enable_values[NUM_ROWS] = {"true", "true", "false"}; + +/* Parameter names - Device.X_T2TEST_Table.AccessPoint.{1,2,3}.{SSID,Status,Enable} */ +static char paramNames[TOTAL_PARAMS][128]; + +static void buildParamNames(void) +{ + for (int row = 0; row < NUM_ROWS; row++) + { + snprintf(paramNames[row * NUM_PARAMS_PER_ROW + 0], 128, TABLE_BASE "%d.SSID", row + 1); + snprintf(paramNames[row * NUM_PARAMS_PER_ROW + 1], 128, TABLE_BASE "%d.Status", row + 1); + snprintf(paramNames[row * NUM_PARAMS_PER_ROW + 2], 128, TABLE_BASE "%d.Enable", row + 1); + } +} + +static const char* getValueForParam(const char *name) +{ + for (int row = 0; row < NUM_ROWS; row++) + { + if (strcmp(name, paramNames[row * NUM_PARAMS_PER_ROW + 0]) == 0) + { + return ssid_values[row]; + } + if (strcmp(name, paramNames[row * NUM_PARAMS_PER_ROW + 1]) == 0) + { + return status_values[row]; + } + if (strcmp(name, paramNames[row * NUM_PARAMS_PER_ROW + 2]) == 0) + { + return enable_values[row]; + } + } + return NULL; +} + +static rbusError_t getHandler(rbusHandle_t h, rbusProperty_t prop, rbusGetHandlerOptions_t *opts) +{ + (void)h; + (void)opts; + const char *name = rbusProperty_GetName(prop); + const char *val = getValueForParam(name); + if (val) + { + rbusValue_t value; + rbusValue_Init(&value); + rbusValue_SetString(value, val); + rbusProperty_SetValue(prop, value); + rbusValue_Release(value); + return RBUS_ERROR_SUCCESS; + } + return RBUS_ERROR_INVALID_INPUT; +} + +static rbusError_t tableGetHandler(rbusHandle_t h, rbusProperty_t prop, rbusGetHandlerOptions_t *opts) +{ + (void)h; + (void)opts; + const char *name = rbusProperty_GetName(prop); + + /* Handle wildcard query - when someone queries Device.X_T2TEST_Table.AccessPoint. */ + if (strcmp(name, TABLE_BASE) == 0) + { + rbusProperty_t current = prop; + int first = 1; + for (int i = 0; i < TOTAL_PARAMS; i++) + { + rbusValue_t val; + rbusValue_Init(&val); + const char *paramVal = getValueForParam(paramNames[i]); + rbusValue_SetString(val, paramVal ? paramVal : ""); + if (first) + { + rbusProperty_SetName(current, paramNames[i]); + rbusProperty_SetValue(current, val); + first = 0; + } + else + { + rbusProperty_t next; + rbusProperty_Init(&next, paramNames[i], val); + rbusProperty_Append(current, next); + rbusProperty_Release(next); + current = next; + } + rbusValue_Release(val); + } + return RBUS_ERROR_SUCCESS; + } + + /* Otherwise, try individual param lookup */ + return getHandler(h, prop, opts); +} + +static rbusError_t setHandler(rbusHandle_t h, rbusProperty_t prop, rbusSetHandlerOptions_t *opts) +{ + (void)h; + (void)prop; + (void)opts; + return RBUS_ERROR_SUCCESS; +} + +static rbusDataElement_t dataElements[TOTAL_PARAMS + 1]; /* +1 for table element */ + +static void exitHandler(int sig) +{ + printf("mock_table_provider: caught signal %d, exiting\n", sig); + rbus_unregDataElements(handle, TOTAL_PARAMS + 1, dataElements); + rbus_close(handle); + exit(0); +} + +int main(void) +{ + rbusError_t rc; + buildParamNames(); + + printf("mock_table_provider: starting...\n"); + + rc = rbus_open(&handle, "mock_table_provider"); + if (rc != RBUS_ERROR_SUCCESS) + { + printf("mock_table_provider: rbus_open failed: %d\n", rc); + return 1; + } + + /* Register individual property elements */ + for (int i = 0; i < TOTAL_PARAMS; i++) + { + dataElements[i].name = paramNames[i]; + dataElements[i].type = RBUS_ELEMENT_TYPE_PROPERTY; + dataElements[i].cbTable.getHandler = getHandler; + dataElements[i].cbTable.setHandler = setHandler; + dataElements[i].cbTable.tableAddRowHandler = NULL; + dataElements[i].cbTable.tableRemoveRowHandler = NULL; + dataElements[i].cbTable.eventSubHandler = NULL; + dataElements[i].cbTable.methodHandler = NULL; + } + + /* Register table-level element for wildcard queries */ + dataElements[TOTAL_PARAMS].name = TABLE_BASE; + dataElements[TOTAL_PARAMS].type = RBUS_ELEMENT_TYPE_PROPERTY; + dataElements[TOTAL_PARAMS].cbTable.getHandler = tableGetHandler; + dataElements[TOTAL_PARAMS].cbTable.setHandler = NULL; + dataElements[TOTAL_PARAMS].cbTable.tableAddRowHandler = NULL; + dataElements[TOTAL_PARAMS].cbTable.tableRemoveRowHandler = NULL; + dataElements[TOTAL_PARAMS].cbTable.eventSubHandler = NULL; + dataElements[TOTAL_PARAMS].cbTable.methodHandler = NULL; + + rc = rbus_regDataElements(handle, TOTAL_PARAMS + 1, dataElements); + if (rc != RBUS_ERROR_SUCCESS) + { + printf("mock_table_provider: rbus_regDataElements failed: %d\n", rc); + rbus_close(handle); + return 1; + } + + printf("mock_table_provider: registered %d elements, running...\n", TOTAL_PARAMS + 1); + + signal(SIGINT, exitHandler); + signal(SIGTERM, exitHandler); + + while (1) + { + sleep(5); + } + + return 0; +} diff --git a/test/functional-tests/tests/report_profiles.py b/test/functional-tests/tests/report_profiles.py index 5d1430066..fbdc4bec5 100644 --- a/test/functional-tests/tests/report_profiles.py +++ b/test/functional-tests/tests/report_profiles.py @@ -2129,3 +2129,126 @@ } ] }''' + +# ============================================================================ +# dataModelTable profiles for L2 integration testing (RDKB-65730) +# ============================================================================ + +# Profile with dataModelTable using explicit index "1,2" +data_datamodeltable_explicit_index = '''{ + "profiles": [ + { + "name": "DT_ExplicitIndex", + "hash": "Hash_DT1", + "value": { + "Name": "DT_ExplicitIndex", + "Description": "DataModelTable with explicit index 1,2", + "Version": "1", + "Protocol": "HTTP", + "EncodingType": "JSON", + "ReportingInterval": 20, + "GenerateNow": true, + "TimeReference": "0001-01-01T00:00:00Z", + "Parameter": [ + { + "type": "dataModel", + "name": "UPTIME", + "reference": "Device.DeviceInfo.UpTime", + "use": "absolute" + }, + { + "type": "dataModelTable", + "reference": "Device.X_T2TEST_Table.AccessPoint.", + "index": "1,2", + "Parameter": [ + { + "type": "dataModel", + "reference": "SSID" + }, + { + "type": "dataModel", + "reference": "Status" + } + ] + } + ], + "HTTP": { + "URL": "https://mockxconf:50051/dataLakeMock/", + "Compression": "None", + "Method": "POST", + "RequestURIParameter": [ + { + "Name": "reportName", + "Reference": "Profile.Name" + } + ] + }, + "JSONEncoding": { + "ReportFormat": "NameValuePair", + "ReportTimestamp": "None" + } + } + } + ] +}''' + +# Profile with dataModelTable using wildcard (no index field) +data_datamodeltable_wildcard = '''{ + "profiles": [ + { + "name": "DT_Wildcard", + "hash": "Hash_DT2", + "value": { + "Name": "DT_Wildcard", + "Description": "DataModelTable wildcard - all rows", + "Version": "1", + "Protocol": "HTTP", + "EncodingType": "JSON", + "ReportingInterval": 20, + "GenerateNow": true, + "TimeReference": "0001-01-01T00:00:00Z", + "Parameter": [ + { + "type": "dataModel", + "name": "UPTIME", + "reference": "Device.DeviceInfo.UpTime", + "use": "absolute" + }, + { + "type": "dataModelTable", + "reference": "Device.X_T2TEST_Table.AccessPoint.", + "Parameter": [ + { + "type": "dataModel", + "reference": "SSID" + }, + { + "type": "dataModel", + "reference": "Status" + }, + { + "type": "dataModel", + "reference": "Enable" + } + ] + } + ], + "HTTP": { + "URL": "https://mockxconf:50051/dataLakeMock/", + "Compression": "None", + "Method": "POST", + "RequestURIParameter": [ + { + "Name": "reportName", + "Reference": "Profile.Name" + } + ] + }, + "JSONEncoding": { + "ReportFormat": "NameValuePair", + "ReportTimestamp": "None" + } + } + } + ] +}''' diff --git a/test/functional-tests/tests/test_datamodeltable.py b/test/functional-tests/tests/test_datamodeltable.py new file mode 100755 index 000000000..c31a41c77 --- /dev/null +++ b/test/functional-tests/tests/test_datamodeltable.py @@ -0,0 +1,238 @@ +#################################################################################### +# If not stated otherwise in this file or this component's Licenses file the +# following copyright and licenses apply: +# +# Copyright 2024 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#################################################################################### + +""" +L2 Integration Tests for dataModelTable feature (RDKB-65730 / AC4) + +Scenarios: +1. Push a profile with dataModelTable (explicit index) -> verify report JSON structure +2. Push a profile with dataModelTable (wildcard) -> verify all rows appear in report +3. Push a profile with dataModelTable while a reporting cycle is active -> verify no crash +4. Build with feature disabled -> push same profile -> verify silently ignored + +Prerequisites: +- mock_table_provider must be compiled and running in the container + (provides Device.X_T2TEST_Table.AccessPoint.{1,2,3}.{SSID,Status,Enable}) +- telemetry2_0 must be built with --enable-dynamic-table-support=yes +""" + +import subprocess +from time import sleep +from datetime import datetime as dt +import pytest + +from basic_constants import * +from helper_functions import * +from report_profiles import * + +LOG_PROFILE_ENABLE = "Successfully enabled profile :" + + +# =========================================================================== +# Scenario 1: dataModelTable with explicit index +# =========================================================================== +@pytest.mark.run(order=1) +def test_datamodeltable_explicit_index(): + """ + Push a profile with dataModelTable (explicit index "1,2") and verify: + - Profile is enabled successfully + - T2 logs show data collection for indexed table parameters + - Report contains structured array with row 1 and row 2 data only + """ + # Setup + clear_T2logs() + kill_telemetry(9) + remove_T2bootup_flag() + clear_persistant_files() + run_telemetry() + sleep(2) + + # Push profile with explicit index + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_datamodeltable_explicit_index) + sleep(25) # Wait for ReportingInterval (20s) + processing time + + # Verify profile was enabled + assert "DT_ExplicitIndex" in grep_T2logs(LOG_PROFILE_ENABLE), \ + "Profile DT_ExplicitIndex was not enabled" + + # Verify dataModelTable parsing succeeded + assert "dataModelTable" not in grep_T2logs("Error"), \ + "Unexpected error related to dataModelTable in logs" + + # Verify data collection happened for indexed params + # T2 should query Device.X_T2TEST_Table.AccessPoint.1.SSID and .2.SSID + log_content = grep_T2logs("Device.X_T2TEST_Table.AccessPoint") + assert "Device.X_T2TEST_Table.AccessPoint" in log_content, \ + "No evidence of table parameter data collection in logs" + + # Verify no crash - telemetry process should still be running + pid = get_pid("telemetry2_0") + assert pid != "", "telemetry2_0 crashed during dataModelTable processing" + + # Cleanup + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_empty_profile) + sleep(2) + + +# =========================================================================== +# Scenario 2: dataModelTable with wildcard (no index) +# =========================================================================== +@pytest.mark.run(order=2) +def test_datamodeltable_wildcard(): + """ + Push a profile with dataModelTable (wildcard - no index) and verify: + - Profile is enabled successfully + - All rows (1,2,3) appear in the report + - Report contains structured array with all sub-parameters + """ + # Setup + clear_T2logs() + kill_telemetry(9) + remove_T2bootup_flag() + clear_persistant_files() + run_telemetry() + sleep(2) + + # Push wildcard profile + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_datamodeltable_wildcard) + sleep(25) # Wait for ReportingInterval (20s) + processing time + + # Verify profile was enabled + assert "DT_Wildcard" in grep_T2logs(LOG_PROFILE_ENABLE), \ + "Profile DT_Wildcard was not enabled" + + # Verify no errors during dataModelTable processing + assert "dataModelTable" not in grep_T2logs("Error"), \ + "Unexpected error related to dataModelTable in logs" + + # Verify wildcard data collection - all rows should be queried + log_content = grep_T2logs("Device.X_T2TEST_Table.AccessPoint") + assert "Device.X_T2TEST_Table.AccessPoint" in log_content, \ + "No evidence of wildcard table parameter collection in logs" + + # Verify no crash - telemetry process should still be running + pid = get_pid("telemetry2_0") + assert pid != "", "telemetry2_0 crashed during wildcard dataModelTable processing" + + # Cleanup + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_empty_profile) + sleep(2) + + +# =========================================================================== +# Scenario 3: dataModelTable while a reporting cycle is active +# =========================================================================== +@pytest.mark.run(order=3) +def test_datamodeltable_during_active_cycle(): + """ + Push a profile with dataModelTable while a reporting cycle is already active. + Verify: + - No crash occurs + - The reporting cycle completes successfully + - The new profile is picked up in the next cycle + """ + # Setup + clear_T2logs() + kill_telemetry(9) + remove_T2bootup_flag() + clear_persistant_files() + run_telemetry() + sleep(2) + + # Push a first profile to start a reporting cycle + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_datamodeltable_wildcard) + sleep(5) # Let reporting cycle start but don't wait for it to complete + + # Now push a different profile mid-cycle + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_datamodeltable_explicit_index) + sleep(25) # Wait for cycle to complete + + # Verify no crash - key assertion + pid = get_pid("telemetry2_0") + assert pid != "", \ + "telemetry2_0 crashed when profile was pushed during active reporting cycle" + + # Verify the new profile was enabled + assert "DT_ExplicitIndex" in grep_T2logs(LOG_PROFILE_ENABLE), \ + "Profile DT_ExplicitIndex was not enabled after mid-cycle push" + + # Verify report completed (look for report generation log) + report_log = grep_T2logs("Report sent successfully") + # Even if report wasn't sent (mock may not accept), no crash is the key requirement + + # Cleanup + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_empty_profile) + sleep(2) + + +# =========================================================================== +# Scenario 4: Feature disabled - dataModelTable silently ignored +# =========================================================================== +@pytest.mark.run(order=4) +def test_datamodeltable_feature_disabled(): + """ + When built WITHOUT --enable-dynamic-table-support, pushing a profile with + dataModelTable should: + - NOT crash + - Silently ignore the dataModelTable entry + - NOT log any error above WARNING level for the ignored entry + - Still process other parameters in the profile normally + + NOTE: This test validates the behavior by checking the T2 logs. + If the binary was built WITH the feature enabled, this test checks that + the "Dynamic table support disabled" debug message is NOT present (meaning + the feature IS active). The CI pipeline runs this scenario against the + disabled build. + """ + # Setup + clear_T2logs() + kill_telemetry(9) + remove_T2bootup_flag() + clear_persistant_files() + run_telemetry() + sleep(2) + + # Push dataModelTable profile + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_datamodeltable_explicit_index) + sleep(25) + + # Verify no crash + pid = get_pid("telemetry2_0") + assert pid != "", "telemetry2_0 crashed processing dataModelTable" + + # Check if this is a feature-disabled build + disabled_msg = grep_T2logs("Dynamic table support disabled") + if disabled_msg: + # Feature is DISABLED in this build - verify silent ignore behavior + # Should NOT have any ERROR level logs about dataModelTable + error_logs = grep_T2logs("Error") + assert "dataModelTable" not in error_logs, \ + "ERROR level log found for dataModelTable in disabled build" + + # The profile should still be enabled (other params are valid) + assert "DT_ExplicitIndex" in grep_T2logs(LOG_PROFILE_ENABLE), \ + "Profile was not enabled even though non-table params are valid" + else: + # Feature is ENABLED - dataModelTable should be processed normally + assert "DT_ExplicitIndex" in grep_T2logs(LOG_PROFILE_ENABLE), \ + "Profile DT_ExplicitIndex was not enabled" + + # Cleanup + rbus_set_data(T2_REPORT_PROFILE_PARAM, "string", data_empty_profile) + sleep(2) diff --git a/test/run_l2.sh b/test/run_l2.sh index 7dbd6410b..0adc81a42 100755 --- a/test/run_l2.sh +++ b/test/run_l2.sh @@ -36,12 +36,26 @@ fi gcc test/functional-tests/tests/app.c -o test/functional-tests/tests/t2_app -ltelemetry_msgsender -lt2utils +# Compile mock table provider for dataModelTable L2 tests +gcc -o test/functional-tests/tests/mock_table_provider test/functional-tests/tests/mock_table_provider.c \ + -I/usr/local/include -I/usr/local/include/rbus \ + -L/usr/local/lib -lrbus -lrbuscore -lrtMessage -lmsgpackc + +# Start mock table provider in background (provides Device.X_T2TEST_Table.AccessPoint.{1,2,3}.*) +test/functional-tests/tests/mock_table_provider & +MOCK_TABLE_PROVIDER_PID=$! +sleep 2 + final_result=0 # removing --exitfirst flag as it is causing the test to exit after first failure pytest -v --json-report --json-report-summary --json-report-file $RESULT_DIR/runs_as_daemon.json test/functional-tests/tests/test_runs_as_daemon.py || final_result=1 pytest -v --json-report --json-report-summary --json-report-file $RESULT_DIR/bootup_sequence.json test/functional-tests/tests/test_bootup_sequence.py || final_result=1 pytest -v --json-report --json-report-summary --json-report-file $RESULT_DIR/xconf_communications.json test/functional-tests/tests/test_xconf_communications.py || final_result=1 pytest -v --json-report --json-report-summary --json-report-file $RESULT_DIR/msg_packet.json test/functional-tests/tests/test_multiprofile_msgpacket.py || final_result=1 +pytest -v --json-report --json-report-summary --json-report-file $RESULT_DIR/datamodeltable.json test/functional-tests/tests/test_datamodeltable.py || final_result=1 + +# Stop mock table provider +kill $MOCK_TABLE_PROVIDER_PID 2>/dev/null if [ $final_result -ne 0 ]; then echo "Some tests failed. Please check the JSON reports in $RESULT_DIR for details."