diff --git a/CMakeLists.txt b/CMakeLists.txt index 12a7f7d1d..bc5a61717 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ if(NOT ENABLE_DATALAYERS) list(FILTER NEURON_SRC_PARSE EXCLUDE REGEX "neu_json_datalayers\\.c$") endif() set(NEURON_BASE_SOURCES + src/base/property.c src/base/tag.c src/base/neu_plugin_common.c src/base/tag_sort.c @@ -232,6 +233,7 @@ add_subdirectory(tests/plugins/c1) add_subdirectory(tests/plugins/s1) add_subdirectory(tests/plugins/sc1) add_subdirectory(tests/plugins/demo_app) +add_subdirectory(tests/plugins/auto_type) # Set sane defaults for multi-lib linux systems include(GNUInstallDirs) diff --git a/docs/api/cn/http.md b/docs/api/cn/http.md index db6443d1d..a9777bede 100644 --- a/docs/api/cn/http.md +++ b/docs/api/cn/http.md @@ -6,6 +6,7 @@ Neuron 将为 IIoT 平台提供一系列 API 服务,用于查询基本信息 ### 数据类型 +* AUTO = 0 * INT8 = 1 * UINT8 = 2 * INT16 = 3 @@ -25,6 +26,16 @@ Neuron 将为 IIoT 平台提供一系列 API 服务,用于查询基本信息 * DWORD = 17 * LWORD = 18 +### AUTO 点位 + +请求自动识别类型时,必须显式设置点位配置字段 `"type": 0`,省略 `type` 不等于 AUTO。驱动必须通过现有点位校验主动支持 AUTO,并返回具体类型的样本;不支持的驱动返回 `2204`。Modbus 不支持 AUTO。本次提供的是框架能力,不代表 OPC UA 等闭源驱动已接入;动态协议驱动仍需分别实现支持。 + +第一笔合法的成功样本确定点位的实际运行时类型,在该点位 cache 生命周期内固定。识别前,读取和写入返回 `3011`(点位未就绪)。之后收到不同语义类型的样本时返回 `3007`(类型不匹配),但不改变已识别类型。通信错误也不会清除类型,恢复原类型的样本后可继续正常采集。点位或组 cache 重建(包括重启)会重新识别,单纯断线不会重置。 + +AUTO 必须用于可读点位;仅写 AUTO 点位返回 `2204`。识别后,单点及批量写入均按具体类型检查输入类型和数值范围,再交给驱动。例如 INT16 可接受整数 `123`,但拒绝 `40000` 和字符串 `"123"`。已排队写入若因点位或组 cache 重建失效,返回 `3011`,不会按另一类型重新解释。 + +配置查询、导出和持久化存储始终保留 `type: 0`,不会回写识别结果,也不新增 `resolved_type` 字段。读取和上报值使用已识别的具体类型,并应用该类型适用的 precision、缩放及地址选项。非数值类型不进行数值缩放,缩放后的输出类型也不会改变设备侧已识别类型。 + ### 波特率 * 115200 = 0 @@ -299,6 +310,123 @@ Neuron 将为 IIoT 平台提供一系列 API 服务,用于查询基本信息 } ``` +## Node 自定义属性 + +自定义属性是节点的 key/value 元数据,南向驱动节点和北向应用节点均支持。四种操作统一使用 **/api/v2/node/properties**,且节点必须已存在。 + +属性的 JSON 值类型在查询和重启后保持不变。节点重命名后,属性随节点迁移到新名称;删除节点时,其属性一并删除。重新创建同名节点不会恢复已删除的属性。 + +### 请求头部 + +**Authorization** Bearer \(登录接口返回的 JWT,鉴权规则与其他节点接口一致) + +**Content-Type** application/json(POST、PUT、DELETE) + +### 键和值 + +* `node`、`key` 为必填非空字符串,不允许包含 NUL(`\u0000`)。同一节点内的属性 key 唯一。 +* `value` 必填,`false`、`0`、`""`、`[]` 均为有效值,不能因其为空或为零而省略。请直接传入 JSON 值,不要将其再次编码成 JSON 字符串。 +* 支持的标量值:字符串、布尔值、整数、浮点数。整数范围为有符号 64 位;浮点数必须是可用双精度表示的有限数值。 +* 支持的一维数组:元素全部为字符串、全部为布尔值或全部为数值。数值数组允许整数和浮点数混用,例如 `[1, 2.5]`。允许空数组。 +* 不接受 `null`、对象、嵌套数组及不同类别混合的数组,例如 `[true, 1]`、`[1, "2"]`。字符串值允许包含转义 NUL(`\u0000`)及其他转义字符。 +* POST 的 `properties` 和 DELETE 的 `keys` 必须为非空数组,批次内不允许重复 key,每项都必须合法。批次操作是原子的:全部成功或完全不修改。 + +### 获取属性 + +*GET* **/api/v2/node/properties?node=modbus-tcp-node** + +查询参数 `node` 必填。节点名称包含特殊字符时需进行 URL 编码。 + +HTTP 200 响应: + +```json +{ + "properties": [ + {"key": "location", "value": "Building A"}, + {"key": "enabled", "value": false}, + {"key": "thresholds", "value": [0, 12.5]} + ] +} +``` + +节点没有属性时返回 `{"properties": []}`。响应值保持原有 JSON 类型;客户端应按 key 识别属性,不依赖数组中的位置。 + +### 新增属性 + +*POST* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "properties": [ + {"key": "location", "value": "Building A"}, + {"key": "enabled", "value": false}, + {"key": "thresholds", "value": [0, 12.5]} + ] +} +``` + +所有 key 都必须是新属性。任一 key 已存在时,拒绝整个批次,不会新增或覆盖任何属性。 + +HTTP 200 响应: + +```json +{"error": 0} +``` + +### 更新单个属性 + +*PUT* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "key": "enabled", + "value": true +} +``` + +替换一个已有属性的值,新值可以变为其他支持的类型。此操作不会创建不存在的属性,也不能重命名 key。 + +HTTP 200 响应: + +```json +{"error": 0} +``` + +### 删除属性 + +*DELETE* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "keys": ["location", "enabled"] +} +``` + +原子地删除指定 key。不存在的 key 会被忽略,即使所有指定 key 都不存在也返回成功。空 `keys` 数组不表示删除全部属性,而是非法请求。 + +HTTP 200 响应: + +```json +{"error": 0} +``` + +### 响应状态 + +| HTTP 状态 | 错误码 | 含义 | +| --- | --- | --- | +| 200 | 0 | POST、PUT、DELETE 成功;GET 返回上述 properties 对象 | +| 400 | 1002 | JSON 请求体、必填字段、key、value 或批次不合法 | +| 400 | 1003 | GET 查询参数缺失或不合法 | +| 401 / 403 | 现有 JWT 错误码 | 令牌缺失、格式错误、过期或无效,遵循现有鉴权规则 | +| 404 | 2003 | 节点不存在,适用于全部四种操作 | +| 404 | 2020 | 属性不存在,仅适用于 PUT | +| 409 | 2019 | 属性已存在,仅适用于 POST;整个批次保持不变 | + +错误响应格式为 `{"error": <错误码>}`。 + ## 获取 Node *GET* /api/v2/node diff --git a/docs/api/english/http.md b/docs/api/english/http.md index f3fe1190d..c26cc8a7f 100644 --- a/docs/api/english/http.md +++ b/docs/api/english/http.md @@ -6,6 +6,7 @@ Neuron provide a series of API services for IIoT platform, to query the basic in ### Data Type +* AUTO = 0 * INT8 = 1 * UINT8 = 2 * INT16 = 3 @@ -25,6 +26,16 @@ Neuron provide a series of API services for IIoT platform, to query the basic in * DWORD = 17 * LWORD = 18 +### AUTO tags + +Set the required tag configuration field explicitly to `"type": 0` to request automatic type detection. Omitting `type` is not equivalent to AUTO. A driver must opt in through its existing tag validation and return concrete sample types; unsupported drivers reject AUTO with error `2204`. Modbus does not support AUTO. This framework support does not mean that closed-source drivers such as OPC UA have been integrated; each dynamic protocol driver requires its own implementation. + +The first valid, successful sample fixes the concrete runtime type for that tag's cache lifecycle. Before detection, reads and writes return `3011` (tag not ready). Later samples with a different semantic type return `3007` (type mismatch), without changing the detected type. Communication errors also preserve it, and sampling recovers when the original type returns. Rebuilding the tag/group cache, including a restart, resets detection; a disconnection alone does not. + +AUTO requires a readable tag; write-only AUTO is rejected with `2204`. After detection, single and batch writes are checked against the concrete type, including input type and numeric range, before the driver receives them. For example, an INT16 tag accepts integer `123`, but rejects `40000` and string `"123"`. Writes invalidated by a tag/group cache rebuild return `3011` rather than being reinterpreted with another type. + +Configuration queries, exports and persistent storage continue to show `type: 0`; detection does not rewrite metadata or add a `resolved_type` field. Read/report values use the detected concrete type and its applicable precision, scaling and address options. Numeric scaling does not apply to nonnumeric types, and a scaled output value does not change the detected device type. + ### Baud * 115200 = 0 @@ -291,6 +302,123 @@ Neuron provide a series of API services for IIoT platform, to query the basic in } ``` +## Node Properties + +Node properties are custom key/value metadata for both southbound driver nodes and northbound application nodes. All four operations use **/api/v2/node/properties** and require an existing node. + +Properties retain their JSON value types across reads and restarts. Renaming a node moves its properties to the new name; deleting a node also deletes its properties. Recreating a node with the same name starts with no properties. + +### Request Headers + +**Authorization** Bearer \ (JWT returned by the login API, using the same authentication rules as other node APIs) + +**Content-Type** application/json (POST, PUT and DELETE) + +### Keys and Values + +* `node` and `key` are required, non-empty strings and must not contain NUL (`\u0000`). Keys are unique within a node. +* `value` is required, including when it is `false`, `0`, `""` or `[]`. It is a JSON value, not a JSON-encoded string. +* Supported scalar values: string, boolean, integer and floating-point number. Integers use the signed 64-bit range; floating-point numbers must be finite and representable as double precision. +* Supported arrays: one-dimensional arrays containing only strings, only booleans, or only numbers. Integers and floating-point numbers may be mixed in a numeric array, for example `[1, 2.5]`. Empty arrays are allowed. +* `null`, objects, nested arrays and mixed-category arrays such as `[true, 1]` or `[1, "2"]` are rejected. JSON string values may contain escaped NUL (`\u0000`) and other escaped characters. +* POST `properties` and DELETE `keys` must be non-empty arrays, without duplicate keys. Every item must be valid. A batch either succeeds in full or makes no changes. + +### Get Properties + +*GET* **/api/v2/node/properties?node=modbus-tcp-node** + +The `node` query parameter is required. URL-encode the node name when needed. + +HTTP 200 response: + +```json +{ + "properties": [ + {"key": "location", "value": "Building A"}, + {"key": "enabled", "value": false}, + {"key": "thresholds", "value": [0, 12.5]} + ] +} +``` + +A node without properties returns `{"properties": []}`. The response contains JSON values with their original types; clients should identify properties by key rather than array position. + +### Add Properties + +*POST* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "properties": [ + {"key": "location", "value": "Building A"}, + {"key": "enabled", "value": false}, + {"key": "thresholds", "value": [0, 12.5]} + ] +} +``` + +All keys must be new. If any key already exists, the entire batch is rejected without adding or replacing any properties. + +HTTP 200 response: + +```json +{"error": 0} +``` + +### Update a Property + +*PUT* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "key": "enabled", + "value": true +} +``` + +Replaces the value of one existing property. The new value may have a different supported type. This operation does not create a missing property or rename its key. + +HTTP 200 response: + +```json +{"error": 0} +``` + +### Delete Properties + +*DELETE* **/api/v2/node/properties** + +```json +{ + "node": "modbus-tcp-node", + "keys": ["location", "enabled"] +} +``` + +Deletes the listed keys atomically. Missing keys are ignored and do not cause an error, including when all listed keys are missing. An empty `keys` array is not a request to delete all properties and is rejected. + +HTTP 200 response: + +```json +{"error": 0} +``` + +### Response Status + +| HTTP status | Error | Meaning | +| --- | --- | --- | +| 200 | 0 | POST, PUT or DELETE succeeded; GET returns the properties object shown above | +| 400 | 1002 | Invalid JSON body, missing required fields or invalid keys/values/batch | +| 400 | 1003 | Missing or invalid GET query parameter | +| 401 / 403 | Existing JWT error codes | Missing, malformed, expired or invalid token, following the existing authentication rules | +| 404 | 2003 | Node does not exist (all four operations) | +| 404 | 2020 | Property does not exist (PUT only) | +| 409 | 2019 | A property already exists (POST only); the entire batch is unchanged | + +Error responses use `{"error": }`. + ## Get Node *GET* /api/v2/node diff --git a/include/neuron/errcodes.h b/include/neuron/errcodes.h index 59ad5bec8..31d1b2069 100644 --- a/include/neuron/errcodes.h +++ b/include/neuron/errcodes.h @@ -72,6 +72,8 @@ typedef enum { NEU_ERR_NODE_TAGS_TOO_LONG = 2016, NEU_ERR_NODE_TAGS_TOO_MANY = 2017, NEU_ERR_NODE_TAGS_INVALID = 2018, + NEU_ERR_NODE_PROPERTY_EXIST = 2019, + NEU_ERR_NODE_PROPERTY_NOT_EXIST = 2020, NEU_ERR_GROUP_ALREADY_SUBSCRIBED = 2101, NEU_ERR_GROUP_NOT_SUBSCRIBE = 2102, diff --git a/include/neuron/msg.h b/include/neuron/msg.h index b9bc4e4dd..95e3248de 100644 --- a/include/neuron/msg.h +++ b/include/neuron/msg.h @@ -31,6 +31,7 @@ extern "C" { #include "define.h" #include "errcodes.h" +#include "property.h" #include "tag.h" #include "type.h" #include @@ -206,6 +207,12 @@ typedef enum neu_reqresp_type { NEU_REQ_SERVER_AUTH_USER_UPDATE_PWD, NEU_REQ_SERVER_AUTH_USER_INFO, NEU_RESP_SERVER_AUTH_USER_INFO, + + NEU_REQ_ADD_NODE_PROPERTIES, + NEU_REQ_DELETE_NODE_PROPERTIES, + NEU_REQ_UPDATE_NODE_PROPERTY, + NEU_REQ_GET_NODE_PROPERTIES, + NEU_RESP_GET_NODE_PROPERTIES, } neu_reqresp_type_e; static const char *neu_reqresp_type_string_t[] = { @@ -374,6 +381,12 @@ static const char *neu_reqresp_type_string_t[] = { "NEU_REQ_SERVER_AUTH_USER_UPDATE_PWD", [NEU_REQ_SERVER_AUTH_USER_INFO] = "NEU_REQ_SERVER_AUTH_USER_INFO", [NEU_RESP_SERVER_AUTH_USER_INFO] = "NEU_RESP_SERVER_AUTH_USER_INFO", + + [NEU_REQ_ADD_NODE_PROPERTIES] = "NEU_REQ_ADD_NODE_PROPERTIES", + [NEU_REQ_DELETE_NODE_PROPERTIES] = "NEU_REQ_DELETE_NODE_PROPERTIES", + [NEU_REQ_UPDATE_NODE_PROPERTY] = "NEU_REQ_UPDATE_NODE_PROPERTY", + [NEU_REQ_GET_NODE_PROPERTIES] = "NEU_REQ_GET_NODE_PROPERTIES", + [NEU_RESP_GET_NODE_PROPERTIES] = "NEU_RESP_GET_NODE_PROPERTIES", }; inline static const char *neu_reqresp_type_string(neu_reqresp_type_e type) @@ -554,6 +567,65 @@ typedef struct neu_req_del_node { char node[NEU_NODE_NAME_LEN]; } neu_req_del_node_t; +typedef struct neu_req_add_node_properties { + char node[NEU_NODE_NAME_LEN]; + size_t n_property; + neu_node_property_t *properties; +} neu_req_add_node_properties_t; + +static inline void +neu_req_add_node_properties_fini(neu_req_add_node_properties_t *req) +{ + neu_node_properties_free(req->properties, req->n_property); + req->properties = NULL; + req->n_property = 0; +} + +typedef struct neu_req_delete_node_properties { + char node[NEU_NODE_NAME_LEN]; + size_t n_key; + char **keys; +} neu_req_delete_node_properties_t; + +static inline void +neu_req_delete_node_properties_fini(neu_req_delete_node_properties_t *req) +{ + for (size_t i = 0; i < req->n_key; ++i) { + free(req->keys[i]); + } + free(req->keys); + req->keys = NULL; + req->n_key = 0; +} + +typedef struct neu_req_update_node_property { + char node[NEU_NODE_NAME_LEN]; + neu_node_property_t property; +} neu_req_update_node_property_t; + +static inline void +neu_req_update_node_property_fini(neu_req_update_node_property_t *req) +{ + neu_node_property_fini(&req->property); +} + +typedef struct neu_req_get_node_properties { + char node[NEU_NODE_NAME_LEN]; +} neu_req_get_node_properties_t; + +typedef struct neu_resp_get_node_properties { + size_t n_property; + neu_node_property_t *properties; +} neu_resp_get_node_properties_t; + +static inline void +neu_resp_get_node_properties_fini(neu_resp_get_node_properties_t *resp) +{ + neu_node_properties_free(resp->properties, resp->n_property); + resp->properties = NULL; + resp->n_property = 0; +} + typedef struct neu_req_get_node { neu_node_type_e type; int n_plugin; diff --git a/include/neuron/persist/persist.h b/include/neuron/persist/persist.h index c0211f895..e4ae7e388 100644 --- a/include/neuron/persist/persist.h +++ b/include/neuron/persist/persist.h @@ -28,6 +28,7 @@ extern "C" { #include "adapter_info.h" #include "persist/json/persist_json_plugin.h" +#include "property.h" typedef neu_json_plugin_req_plugin_t neu_persist_plugin_info_t; @@ -264,6 +265,17 @@ int neu_persister_update_node_tags(const char *node_name, const char *tags); */ int neu_persister_update_node_state(const char *node_name, int state); +int neu_persister_add_node_properties(const char * node, + const neu_node_property_t *properties, + size_t n_property); +int neu_persister_delete_node_properties(const char * node, + const char *const *keys, size_t n_key); +int neu_persister_update_node_property(const char * node, + const neu_node_property_t *property); +int neu_persister_load_node_properties(const char * node, + neu_node_property_t **properties, + size_t * n_property); + /** * Persist plugins. * @param plugin_infos vector of neu_persist_plugin_info_t. diff --git a/include/neuron/property.h b/include/neuron/property.h new file mode 100644 index 000000000..b6513be51 --- /dev/null +++ b/include/neuron/property.h @@ -0,0 +1,73 @@ +#ifndef NEURON_PROPERTY_H +#define NEURON_PROPERTY_H + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif +char *neu_node_property_json_dump(const json_t *value); +#ifdef __cplusplus +} +#endif + +typedef struct { + char *key; + char *value_json; +} neu_node_property_t; + +static inline void neu_node_property_fini(neu_node_property_t *property) +{ + free(property->key); + free(property->value_json); + property->key = NULL; + property->value_json = NULL; +} + +static inline void neu_node_properties_free(neu_node_property_t *properties, + size_t n_property) +{ + for (size_t i = 0; i < n_property; ++i) { + neu_node_property_fini(&properties[i]); + } + free(properties); +} + +static inline int neu_node_property_value_kind(const json_t *value) +{ + if (json_is_string(value)) { + return 1; + } + if (json_is_boolean(value)) { + return 2; + } + if (json_is_number(value)) { + return 3; + } + return 0; +} + +static inline bool neu_node_property_value_valid(const json_t *value) +{ + if (neu_node_property_value_kind(value) != 0) { + return true; + } + if (!json_is_array(value)) { + return false; + } + int kind = 0; + for (size_t i = 0; i < json_array_size(value); ++i) { + int item_kind = neu_node_property_value_kind(json_array_get(value, i)); + if (item_kind == 0 || (kind != 0 && kind != item_kind)) { + return false; + } + kind = item_kind; + } + return true; +} + +#endif diff --git a/include/neuron/type.h b/include/neuron/type.h index f2706747d..cbad7692c 100644 --- a/include/neuron/type.h +++ b/include/neuron/type.h @@ -36,6 +36,7 @@ extern "C" { #include typedef enum { + NEU_TYPE_AUTO = 0, NEU_TYPE_INT8 = 1, NEU_TYPE_UINT8 = 2, NEU_TYPE_INT16 = 3, @@ -76,6 +77,8 @@ typedef enum { inline static const char *neu_type_string(neu_type_e type) { switch (type) { + case NEU_TYPE_AUTO: + return "NEU_TYPE_AUTO"; case NEU_TYPE_INT8: return "NEU_TYPE_INT8"; case NEU_TYPE_UINT8: @@ -340,9 +343,21 @@ static inline char *neu_value_str(neu_type_e type, neu_value_u value) } typedef struct { - neu_type_e type; - neu_value_u value; - uint8_t precision; + int64_t timestamp; + int64_t source_timestamp; + int64_t server_timestamp; + int32_t quality; + bool has_timestamp; + bool has_source_timestamp; + bool has_server_timestamp; + bool has_quality; +} neu_tag_sample_attrs_t; + +typedef struct { + neu_type_e type; + neu_value_u value; + uint8_t precision; + neu_tag_sample_attrs_t attrs; } neu_dvalue_t; static inline bool neu_dvalue_is_array(const neu_dvalue_t *dvalue) @@ -508,6 +523,7 @@ static inline void neu_copy_dvalue(const neu_dvalue_t *src, neu_dvalue_t *dst) dst->type = src->type; dst->precision = src->precision; dst->value = src->value; + dst->attrs = src->attrs; // value need deep copy switch (src->type) { diff --git a/persistence/0160_2.16.1_node_properties.sql b/persistence/0160_2.16.1_node_properties.sql new file mode 100644 index 000000000..742a0f010 --- /dev/null +++ b/persistence/0160_2.16.1_node_properties.sql @@ -0,0 +1,8 @@ +CREATE TABLE node_properties ( + node_name TEXT NOT NULL, + key TEXT NOT NULL CHECK(length(key) > 0), + value_json TEXT NOT NULL, + PRIMARY KEY (node_name, key), + FOREIGN KEY (node_name) REFERENCES nodes(name) + ON UPDATE CASCADE ON DELETE CASCADE +); diff --git a/plugins/restful/adapter_handle.c b/plugins/restful/adapter_handle.c index cc6d3b9b7..465d2bcbb 100644 --- a/plugins/restful/adapter_handle.c +++ b/plugins/restful/adapter_handle.c @@ -713,6 +713,122 @@ void handle_put_drivers(nng_aio *aio) }) } +void handle_add_node_properties(nng_aio *aio) +{ + NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT( + aio, neu_json_add_node_properties_req_t, + neu_json_decode_add_node_properties_req, { + neu_reqresp_head_t header = { 0 }; + neu_req_add_node_properties_t cmd = { 0 }; + header.ctx = aio; + header.type = NEU_REQ_ADD_NODE_PROPERTIES; + header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM; + strcpy(cmd.node, req->node); + cmd.properties = req->properties; + cmd.n_property = req->n_property; + req->properties = NULL; + req->n_property = 0; + if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) { + neu_req_add_node_properties_fini(&cmd); + NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, { + neu_http_response(aio, error_code.error, result_error); + }); + } + }) +} + +void handle_delete_node_properties(nng_aio *aio) +{ + NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT( + aio, neu_json_delete_node_properties_req_t, + neu_json_decode_delete_node_properties_req, { + neu_reqresp_head_t header = { 0 }; + neu_req_delete_node_properties_t cmd = { 0 }; + header.ctx = aio; + header.type = NEU_REQ_DELETE_NODE_PROPERTIES; + header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM; + strcpy(cmd.node, req->node); + cmd.keys = req->keys; + cmd.n_key = req->n_key; + req->keys = NULL; + req->n_key = 0; + if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) { + neu_req_delete_node_properties_fini(&cmd); + NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, { + neu_http_response(aio, error_code.error, result_error); + }); + } + }) +} + +void handle_update_node_property(nng_aio *aio) +{ + NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT( + aio, neu_json_update_node_property_req_t, + neu_json_decode_update_node_property_req, { + neu_reqresp_head_t header = { 0 }; + neu_req_update_node_property_t cmd = { 0 }; + header.ctx = aio; + header.type = NEU_REQ_UPDATE_NODE_PROPERTY; + header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM; + strcpy(cmd.node, req->node); + cmd.property = req->property; + req->property.key = NULL; + req->property.value_json = NULL; + if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) { + neu_req_update_node_property_fini(&cmd); + NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, { + neu_http_response(aio, error_code.error, result_error); + }); + } + }) +} + +void handle_get_node_properties(nng_aio *aio) +{ + NEU_VALIDATE_JWT(aio); + + neu_req_get_node_properties_t cmd = { 0 }; + ssize_t length = + neu_http_get_param_str(aio, "node", cmd.node, sizeof(cmd.node)); + if (length <= 0 || length >= (ssize_t) sizeof(cmd.node) || + strlen(cmd.node) != (size_t) length) { + NEU_JSON_RESPONSE_ERROR(NEU_ERR_PARAM_IS_WRONG, { + neu_http_response(aio, error_code.error, result_error); + }); + return; + } + neu_reqresp_head_t header = { + .ctx = aio, + .type = NEU_REQ_GET_NODE_PROPERTIES, + .otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM, + }; + if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) { + NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, { + neu_http_response(aio, error_code.error, result_error); + }); + } +} + +void handle_get_node_properties_resp(nng_aio * aio, + neu_resp_get_node_properties_t *properties) +{ + neu_json_get_node_properties_resp_t resp = { + .n_property = properties->n_property, + .properties = properties->properties, + }; + char *result = NULL; + if (neu_json_encode_get_node_properties_resp(&resp, &result) != 0) { + NEU_JSON_RESPONSE_ERROR(NEU_ERR_EINTERNAL, { + neu_http_response(aio, error_code.error, result_error); + }); + } else { + neu_http_ok(aio, result); + } + free(result); + neu_resp_get_node_properties_fini(properties); +} + void handle_put_node_tag(nng_aio *aio) { neu_plugin_t *plugin = neu_rest_get_plugin(); diff --git a/plugins/restful/adapter_handle.h b/plugins/restful/adapter_handle.h index 4768ce00a..f11bfd19e 100644 --- a/plugins/restful/adapter_handle.h +++ b/plugins/restful/adapter_handle.h @@ -43,4 +43,11 @@ void handle_get_nodes_state_resp(nng_aio * aio, void handle_put_drivers(nng_aio *aio); void handle_put_node_tag(nng_aio *aio); +void handle_add_node_properties(nng_aio *aio); +void handle_delete_node_properties(nng_aio *aio); +void handle_update_node_property(nng_aio *aio); +void handle_get_node_properties(nng_aio *aio); +void handle_get_node_properties_resp( + nng_aio *aio, neu_resp_get_node_properties_t *properties); + #endif \ No newline at end of file diff --git a/plugins/restful/handle.c b/plugins/restful/handle.c index bec0820f8..05f1dc509 100644 --- a/plugins/restful/handle.c +++ b/plugins/restful/handle.c @@ -109,6 +109,9 @@ static struct neu_http_handler cors_handler[] = { { .url = "/api/v2/node/setting", }, + { + .url = "/api/v2/node/properties", + }, { .url = "/api/v2/node/ctl", }, @@ -763,6 +766,30 @@ static struct neu_http_handler rest_handlers[] = { .url = "/api/v2/simulator/export", .value.handler = handle_simulator_export, }, + { + .method = NEU_HTTP_METHOD_POST, + .type = NEU_HTTP_HANDLER_FUNCTION, + .url = "/api/v2/node/properties", + .value.handler = handle_add_node_properties, + }, + { + .method = NEU_HTTP_METHOD_PUT, + .type = NEU_HTTP_HANDLER_FUNCTION, + .url = "/api/v2/node/properties", + .value.handler = handle_update_node_property, + }, + { + .method = NEU_HTTP_METHOD_DELETE, + .type = NEU_HTTP_HANDLER_FUNCTION, + .url = "/api/v2/node/properties", + .value.handler = handle_delete_node_properties, + }, + { + .method = NEU_HTTP_METHOD_GET, + .type = NEU_HTTP_HANDLER_FUNCTION, + .url = "/api/v2/node/properties", + .value.handler = handle_get_node_properties, + }, { .method = NEU_HTTP_METHOD_PUT, .type = NEU_HTTP_HANDLER_FUNCTION, diff --git a/plugins/restful/rest.c b/plugins/restful/rest.c index 6dcecd836..f4a59accf 100644 --- a/plugins/restful/rest.c +++ b/plugins/restful/rest.c @@ -332,6 +332,13 @@ static int dashb_plugin_request(neu_plugin_t * plugin, neu_otel_scope_set_status_code2(scope, NEU_OTEL_STATUS_OK, 0); } break; + case NEU_RESP_GET_NODE_PROPERTIES: + handle_get_node_properties_resp( + header->ctx, (neu_resp_get_node_properties_t *) data); + if (neu_otel_control_is_started() && trace) { + neu_otel_scope_set_status_code2(scope, NEU_OTEL_STATUS_OK, 0); + } + break; case NEU_RESP_GET_NODE_SETTING: handle_get_node_setting_resp(header->ctx, (neu_resp_get_node_setting_t *) data); diff --git a/src/adapter/adapter.c b/src/adapter/adapter.c index aae322bba..6606fa80f 100644 --- a/src/adapter/adapter.c +++ b/src/adapter/adapter.c @@ -620,6 +620,10 @@ static int adapter_command(neu_adapter_t *adapter, neu_reqresp_head_t header, snprintf(pheader->receiver, sizeof(pheader->receiver), "%s", cmd->node); break; } + case NEU_REQ_ADD_NODE_PROPERTIES: + case NEU_REQ_DELETE_NODE_PROPERTIES: + case NEU_REQ_UPDATE_NODE_PROPERTY: + case NEU_REQ_GET_NODE_PROPERTIES: case NEU_REQ_CHECK_SCHEMA: case NEU_REQ_GET_DRIVER_GROUP: case NEU_REQ_GET_SUB_DRIVER_TAGS: { @@ -768,6 +772,7 @@ static int adapter_command(neu_adapter_t *adapter, neu_reqresp_head_t header, break; } case NEU_RESP_ERROR: + case NEU_RESP_GET_NODE_PROPERTIES: case NEU_RESP_SERVER_AUTH_USER_INFO: case NEU_RESP_SERVER_AUTH_SWITCH_STATUS: case NEU_RESP_SERVER_SECURITY_POLICY_STATUS: @@ -951,6 +956,7 @@ static int adapter_loop(enum neu_event_io_type type, int fd, void *usr_data) case NEU_RESP_GET_NODE_STATE: case NEU_RESP_GET_NODES_STATE: case NEU_RESP_GET_NODE_SETTING: + case NEU_RESP_GET_NODE_PROPERTIES: case NEU_REQ_UPDATE_GROUP: case NEU_RESP_GET_SUBSCRIBE_GROUP: case NEU_RESP_GET_DRIVER_SUBSCRIBE_GROUP: diff --git a/src/adapter/driver/cache.c b/src/adapter/driver/cache.c index 341169313..cf98709a3 100644 --- a/src/adapter/driver/cache.c +++ b/src/adapter/driver/cache.c @@ -27,6 +27,7 @@ #include "utils/uthash.h" #include "define.h" +#include "errcodes.h" #include "tag.h" #include "cache.h" @@ -44,6 +45,14 @@ struct elem { neu_dvalue_t value; neu_dvalue_t value_old; + bool has_old; + bool configured; + neu_type_e config_type; + neu_type_e resolved_type; + uint64_t generation; + uint8_t precision; + double decimal; + double bias; neu_tag_meta_t *metas; int n_meta; @@ -62,10 +71,100 @@ struct neu_driver_cache { pthread_mutex_t mtx; group_trace_t * trace_table; struct elem * table; + uint64_t generation; }; -// static void update_tag_error(neu_driver_cache_t *cache, const char *group, -// const char *tag, int64_t timestamp, int error); +static void value_free(neu_dvalue_t *value) +{ + if (value->type == NEU_TYPE_PTR) { + free(value->value.ptr.ptr); + } else { + neu_free_dvalue(value); + } + memset(value, 0, sizeof(*value)); +} + +static void value_copy(const neu_dvalue_t *src, neu_dvalue_t *dst) +{ + if (src->type >= NEU_TYPE_ARRAY_INT8 && + src->type <= NEU_TYPE_ARRAY_STRING && src->value.bools.length == 0) { + *dst = *src; + dst->value.bools.bools = NULL; + return; + } + neu_copy_dvalue(src, dst); + if (src->type == NEU_TYPE_PTR) { + dst->value.ptr.ptr = calloc(1, src->value.ptr.length); + if (src->value.ptr.length != 0) { + memcpy(dst->value.ptr.ptr, src->value.ptr.ptr, + src->value.ptr.length); + } + } +} + +static void metas_free(neu_tag_meta_t *metas, int n_meta) +{ + for (int i = 0; i < n_meta; i++) { + value_free(&metas[i].value); + } + free(metas); +} + +static void elem_clear(struct elem *elem) +{ + value_free(&elem->value); + value_free(&elem->value_old); + metas_free(elem->metas, elem->n_meta); + elem->metas = NULL; + elem->n_meta = 0; + elem->has_old = false; +} + +static bool real_type(neu_type_e type) +{ + return (type >= NEU_TYPE_INT8 && type <= NEU_TYPE_BYTES) || + (type >= NEU_TYPE_WORD && type <= NEU_TYPE_LWORD) || + (type >= NEU_TYPE_TIME && type <= NEU_TYPE_ARRAY_STRING) || + type == NEU_TYPE_CUSTOM; +} + +static neu_type_e semantic_type(const neu_dvalue_t *value) +{ + if (value->type != NEU_TYPE_PTR) { + return value->type; + } + return value->value.ptr.type == NEU_TYPE_STRING || + value->value.ptr.type == NEU_TYPE_BYTES + ? value->value.ptr.type + : NEU_TYPE_AUTO; +} + +static int sample_validate(const struct elem *elem, const neu_dvalue_t *value) +{ + neu_type_e type = semantic_type(value); + if (!real_type(type)) { + return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; + } + if (elem->resolved_type != NEU_TYPE_AUTO && elem->resolved_type != type) { + return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; + } + if ((value->type == NEU_TYPE_FLOAT && !isfinite(value->value.f32)) || + (value->type == NEU_TYPE_DOUBLE && !isfinite(value->value.d64))) { + return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; + } + if (type < NEU_TYPE_INT8 || type > NEU_TYPE_DOUBLE) { + if (elem->decimal != 0) { + return NEU_ERR_TAG_DECIMAL_INVALID; + } + if (elem->bias != 0) { + return NEU_ERR_TAG_BIAS_INVALID; + } + if (elem->precision != 0) { + return NEU_ERR_TAG_PRECISION_INVALID; + } + } + return NEU_ERR_SUCCESS; +} inline static tkey_t to_key(const char *group, const char *tag) { @@ -95,33 +194,7 @@ void neu_driver_cache_destroy(neu_driver_cache_t *cache) HASH_ITER(hh, cache->table, elem, tmp) { HASH_DEL(cache->table, elem); - if (elem->value.type == NEU_TYPE_PTR) { - if (elem->value.value.ptr.ptr != NULL) { - free(elem->value.value.ptr.ptr); - elem->value.value.ptr.ptr = NULL; - } - } else if (elem->value.type == NEU_TYPE_CUSTOM) { - if (elem->value.value.json != NULL) { - json_decref(elem->value.value.json); - elem->value.value.json = NULL; - } - } else if (elem->value.type == NEU_TYPE_ARRAY_STRING) { - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - free(elem->value.value.strs.strs[i]); - elem->value.value.strs.strs[i] = NULL; - } - free(elem->value.value.strs.strs); - } else if (NEU_TYPE_ARRAY_CHAR < elem->value.type && - elem->value.type < NEU_TYPE_ARRAY_STRING) { - free(elem->value.value.bools.bools); - } - - if (elem->metas != NULL) { - for (int i = 0; i < elem->n_meta; i++) { - neu_free_dvalue(&elem->metas[i].value); - } - free(elem->metas); - } + elem_clear(elem); free(elem); } @@ -142,35 +215,93 @@ void neu_driver_cache_destroy(neu_driver_cache_t *cache) free(cache); } -// void neu_driver_cache_error(neu_driver_cache_t *cache, const char *group, -// const char *tag, int64_t timestamp, int32_t error) -//{ -// update_tag_error(cache, group, tag, timestamp, error); -//} - -void neu_driver_cache_add(neu_driver_cache_t *cache, const char *group, - const char *tag, neu_dvalue_t value) +static void cache_add(neu_driver_cache_t *cache, const char *group, + const char *tag, const neu_datatag_t *config, + neu_dvalue_t initial) { struct elem *elem = NULL; tkey_t key = to_key(group, tag); pthread_mutex_lock(&cache->mtx); HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem); - if (elem == NULL) { - elem = calloc(1, sizeof(struct elem)); - - snprintf(elem->key.group, sizeof(elem->key.group), "%s", group); - snprintf(elem->key.tag, sizeof(elem->key.tag), "%s", tag); - + elem = calloc(1, sizeof(struct elem)); + elem->key = key; HASH_ADD(hh, cache->table, key, sizeof(tkey_t), elem); + } else { + elem_clear(elem); + } + + elem->timestamp = 0; + elem->changed = false; + elem->configured = config != NULL; + elem->config_type = config ? config->type : NEU_TYPE_AUTO; + elem->resolved_type = + config && real_type(config->type) ? config->type : NEU_TYPE_AUTO; + elem->generation = ++cache->generation; + elem->precision = config ? config->precision : initial.precision; + elem->decimal = config ? config->decimal : 0; + elem->bias = config ? config->bias : 0; + if (config && config->type == NEU_TYPE_AUTO && + initial.type != NEU_TYPE_ERROR) { + neu_type_e type = semantic_type(&initial); + int ret = sample_validate(elem, &initial); + if (ret == NEU_ERR_SUCCESS) { + elem->resolved_type = type; + } else { + neu_free_dvalue(&initial); + memset(&initial, 0, sizeof(initial)); + initial.type = NEU_TYPE_ERROR; + initial.value.i32 = ret; + } + } + elem->value = initial; + if (initial.type == NEU_TYPE_PTR) { + value_copy(&initial, &elem->value); + } + if (config) { + elem->value.precision = elem->precision; + elem->changed = config->type == NEU_TYPE_AUTO && + elem->resolved_type != NEU_TYPE_AUTO; + } + if (sub_filter_err && elem->value.type != NEU_TYPE_ERROR) { + value_copy(&elem->value, &elem->value_old); + elem->has_old = true; } + pthread_mutex_unlock(&cache->mtx); +} - elem->timestamp = 0; - elem->changed = false; - elem->value = value; +void neu_driver_cache_add(neu_driver_cache_t *cache, const char *group, + const char *tag, neu_dvalue_t value) +{ + cache_add(cache, group, tag, NULL, value); +} + +void neu_driver_cache_add_tag(neu_driver_cache_t *cache, const char *group, + const neu_datatag_t *tag, neu_dvalue_t initial) +{ + cache_add(cache, group, tag->name, tag, initial); +} +int neu_driver_cache_type_get(neu_driver_cache_t *cache, const char *group, + const char *tag, neu_type_e *type, + uint64_t *generation) +{ + struct elem *elem = NULL; + tkey_t key = to_key(group, tag); + int ret = NEU_ERR_PLUGIN_TAG_NOT_READY; + + pthread_mutex_lock(&cache->mtx); + HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem); + if (elem != NULL) { + *type = elem->resolved_type; + *generation = elem->generation; + if (real_type(elem->resolved_type)) { + ret = NEU_ERR_SUCCESS; + } + } pthread_mutex_unlock(&cache->mtx); + return ret; } void neu_driver_cache_update_trace(neu_driver_cache_t *cache, const char *group, @@ -219,650 +350,182 @@ void *neu_driver_cache_get_trace(neu_driver_cache_t *cache, const char *group) return trace; } -bool neu_driver_cache_update_change(neu_driver_cache_t *cache, +static bool value_equal(const neu_dvalue_t *previous, neu_dvalue_t *value) +{ + if (previous->type != value->type) { + return false; + } + if (value->type >= NEU_TYPE_ARRAY_INT8 && + value->type <= NEU_TYPE_ARRAY_STRING && + value->value.bools.length == 0) { + return previous->value.bools.length == 0; + } + if (value->type == NEU_TYPE_PTR && value->value.ptr.length == 0) { + return previous->value.ptr.length == 0 && + previous->value.ptr.type == value->value.ptr.type; + } + switch (value->type) { + case NEU_TYPE_STRING: + case NEU_TYPE_TIME: + case NEU_TYPE_DATA_AND_TIME: + return strncmp(previous->value.str, value->value.str, + sizeof(value->value.str)) == 0; + case NEU_TYPE_ARRAY_CHAR: + return memcmp(previous->value.str, value->value.str, + sizeof(value->value.str)) == 0; + case NEU_TYPE_FLOAT: + return previous->precision == 0 + ? previous->value.f32 == value->value.f32 + : !(fabs(previous->value.f32 - value->value.f32) > + pow(0.1, previous->precision)); + case NEU_TYPE_DOUBLE: + return previous->precision == 0 + ? previous->value.d64 == value->value.d64 + : !(fabs(previous->value.d64 - value->value.d64) > + pow(0.1, previous->precision)); + case NEU_TYPE_ARRAY_FLOAT: + return previous->value.f32s.length == value->value.f32s.length && + (value->value.f32s.length == 0 || + memcmp(previous->value.f32s.f32s, value->value.f32s.f32s, + value->value.f32s.length * sizeof(float)) == 0); + case NEU_TYPE_ARRAY_DOUBLE: + return previous->value.f64s.length == value->value.f64s.length && + (value->value.f64s.length == 0 || + memcmp(previous->value.f64s.f64s, value->value.f64s.f64s, + value->value.f64s.length * sizeof(double)) == 0); + case NEU_TYPE_ERROR: + return false; + default: { + neu_dvalue_t other = *value; + other.precision = previous->precision; + return neu_equal_dvalue(previous, &other); + } + } +} + +int neu_driver_cache_update_checked(neu_driver_cache_t *cache, const char *group, const char *tag, int64_t timestamp, neu_dvalue_t value, neu_tag_meta_t *metas, int n_meta, - bool change) + bool force, bool *changed) { struct elem *elem = NULL; tkey_t key = to_key(group, tag); bool tag_changed = false; + int ret = NEU_ERR_SUCCESS; + + if (changed) { + *changed = false; + } + if (!value.attrs.has_timestamp) { + value.attrs.timestamp = 0; + } + if (!value.attrs.has_source_timestamp) { + value.attrs.source_timestamp = 0; + } + if (!value.attrs.has_server_timestamp) { + value.attrs.server_timestamp = 0; + } + if (!value.attrs.has_quality) { + value.attrs.quality = 0; + } pthread_mutex_lock(&cache->mtx); HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem); - if (elem != NULL) { - elem->timestamp = timestamp; - - if (sub_filter_err && value.type == NEU_TYPE_ERROR) { - goto error_not_report; - } - - if ((!sub_filter_err && elem->value.type != value.type) || - (sub_filter_err && elem->value.type != value.type && - elem->value.type != NEU_TYPE_ERROR)) { - elem->changed = true; - tag_changed = true; - } else if (sub_filter_err && elem->value.type != value.type && - elem->value.type == NEU_TYPE_ERROR) { - switch (value.type) { - case NEU_TYPE_INT8: - case NEU_TYPE_UINT8: - case NEU_TYPE_INT16: - case NEU_TYPE_UINT16: - case NEU_TYPE_INT32: - case NEU_TYPE_UINT32: - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - case NEU_TYPE_BIT: - case NEU_TYPE_BOOL: - case NEU_TYPE_STRING: - case NEU_TYPE_TIME: - case NEU_TYPE_DATA_AND_TIME: - case NEU_TYPE_WORD: - case NEU_TYPE_DWORD: - case NEU_TYPE_LWORD: - case NEU_TYPE_ARRAY_CHAR: - if (memcmp(&elem->value_old.value, &value.value, - sizeof(value.value)) != 0) { - elem->changed = true; - tag_changed = true; - } - break; - case NEU_TYPE_BYTES: - if (elem->value_old.value.bytes.length != - value.value.bytes.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.bytes.bytes, - value.value.bytes.bytes, - value.value.bytes.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_BOOL: - if (elem->value_old.value.bools.length != - value.value.bools.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.bools.bools, - value.value.bools.bools, - value.value.bools.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT8: - if (elem->value_old.value.i8s.length != - value.value.i8s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.i8s.i8s, - value.value.i8s.i8s, - value.value.i8s.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT8: - if (elem->value_old.value.u8s.length != - value.value.u8s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.u8s.u8s, - value.value.u8s.u8s, - value.value.u8s.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT16: - if (elem->value_old.value.i16s.length != - value.value.i16s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.i16s.i16s, - value.value.i16s.i16s, - value.value.i16s.length * sizeof(int16_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT16: - if (elem->value_old.value.u16s.length != - value.value.u16s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.u16s.u16s, - value.value.u16s.u16s, - value.value.u16s.length * sizeof(uint16_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT32: - if (elem->value_old.value.i32s.length != - value.value.i32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.i32s.i32s, - value.value.i32s.i32s, - value.value.i32s.length * sizeof(int32_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT32: - if (elem->value_old.value.u32s.length != - value.value.u32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.u32s.u32s, - value.value.u32s.u32s, - value.value.u32s.length * sizeof(uint32_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT64: - if (elem->value_old.value.i64s.length != - value.value.i64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.i64s.i64s, - value.value.i64s.i64s, - value.value.i64s.length * sizeof(int64_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT64: - if (elem->value_old.value.u64s.length != - value.value.u64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.u64s.u64s, - value.value.u64s.u64s, - value.value.u64s.length * sizeof(uint64_t)) != - 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_FLOAT: - if (elem->value_old.value.f32s.length != - value.value.f32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.f32s.f32s, - value.value.f32s.f32s, - value.value.f32s.length * sizeof(float)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_DOUBLE: - if (elem->value_old.value.f64s.length != - value.value.f64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.f64s.f64s, - value.value.f64s.f64s, - value.value.f64s.length * sizeof(double)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_STRING: - if (elem->value_old.value.strs.length != - value.value.strs.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.strs.strs, - value.value.strs.strs, - value.value.strs.length * sizeof(char *)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_CUSTOM: { - if (json_equal(elem->value_old.value.json, value.value.json) != - 1) { - elem->changed = true; - tag_changed = true; - } - break; - } - case NEU_TYPE_PTR: { - if (elem->value_old.value.ptr.length != - value.value.ptr.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value_old.value.ptr.ptr, - value.value.ptr.ptr, - value.value.ptr.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - - break; - } - case NEU_TYPE_FLOAT: - if (elem->value_old.precision == 0) { - elem->changed = - elem->value_old.value.f32 != value.value.f32; - tag_changed = elem->changed; - } else { - if (fabs(elem->value_old.value.f32 - value.value.f32) > - pow(0.1, elem->value_old.precision)) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_DOUBLE: - if (elem->value_old.precision == 0) { - elem->changed = - elem->value_old.value.d64 != value.value.d64; - tag_changed = elem->changed; - } else { - if (fabs(elem->value_old.value.d64 - value.value.d64) > - pow(0.1, elem->value_old.precision)) { - elem->changed = true; - tag_changed = true; - } - } - - break; - case NEU_TYPE_ERROR: - break; - } - } else { - switch (value.type) { - case NEU_TYPE_INT8: - case NEU_TYPE_UINT8: - case NEU_TYPE_INT16: - case NEU_TYPE_UINT16: - case NEU_TYPE_INT32: - case NEU_TYPE_UINT32: - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - case NEU_TYPE_BIT: - case NEU_TYPE_BOOL: - case NEU_TYPE_STRING: - case NEU_TYPE_TIME: - case NEU_TYPE_DATA_AND_TIME: - case NEU_TYPE_WORD: - case NEU_TYPE_DWORD: - case NEU_TYPE_LWORD: - case NEU_TYPE_ARRAY_CHAR: - if (memcmp(&elem->value.value, &value.value, - sizeof(value.value)) != 0) { - elem->changed = true; - tag_changed = true; - } - break; - case NEU_TYPE_BYTES: - if (elem->value.value.bytes.length != - value.value.bytes.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.bytes.bytes, - value.value.bytes.bytes, - value.value.bytes.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_BOOL: - if (elem->value.value.bools.length != - value.value.bools.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.bools.bools, - value.value.bools.bools, - value.value.bools.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT8: - if (elem->value.value.i8s.length != value.value.i8s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.i8s.i8s, value.value.i8s.i8s, - value.value.i8s.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT8: - if (elem->value.value.u8s.length != value.value.u8s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.u8s.u8s, value.value.u8s.u8s, - value.value.u8s.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT16: - if (elem->value.value.i16s.length != value.value.i16s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.i16s.i16s, value.value.i16s.i16s, - value.value.i16s.length * sizeof(int16_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT16: - if (elem->value.value.u16s.length != value.value.u16s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.u16s.u16s, value.value.u16s.u16s, - value.value.u16s.length * sizeof(uint16_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT32: - if (elem->value.value.i32s.length != value.value.i32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.i32s.i32s, value.value.i32s.i32s, - value.value.i32s.length * sizeof(int32_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT32: - if (elem->value.value.u32s.length != value.value.u32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.u32s.u32s, value.value.u32s.u32s, - value.value.u32s.length * sizeof(uint32_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_INT64: - if (elem->value.value.i64s.length != value.value.i64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.i64s.i64s, value.value.i64s.i64s, - value.value.i64s.length * sizeof(int64_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_UINT64: - if (elem->value.value.u64s.length != value.value.u64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp( - elem->value.value.u64s.u64s, value.value.u64s.u64s, - value.value.u64s.length * sizeof(uint64_t)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_FLOAT: - if (elem->value.value.f32s.length != value.value.f32s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.f32s.f32s, - value.value.f32s.f32s, - value.value.f32s.length * sizeof(float)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_DOUBLE: - if (elem->value.value.f64s.length != value.value.f64s.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.f64s.f64s, - value.value.f64s.f64s, - value.value.f64s.length * sizeof(double)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_ARRAY_STRING: - if (elem->value.value.strs.length != value.value.strs.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.strs.strs, - value.value.strs.strs, - value.value.strs.length * sizeof(char *)) != 0) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_PTR: { - if (elem->value.value.ptr.length != value.value.ptr.length) { - elem->changed = true; - tag_changed = true; - } else { - if (memcmp(elem->value.value.ptr.ptr, value.value.ptr.ptr, - value.value.ptr.length) != 0) { - elem->changed = true; - tag_changed = true; - } - } - - break; - } - case NEU_TYPE_CUSTOM: { - if (json_equal(elem->value.value.json, value.value.json) != 1) { - elem->changed = true; - tag_changed = true; - } - break; - } - case NEU_TYPE_FLOAT: - if (elem->value.precision == 0) { - elem->changed = elem->value.value.f32 != value.value.f32; - tag_changed = elem->changed; - } else { - if (fabs(elem->value.value.f32 - value.value.f32) > - pow(0.1, elem->value.precision)) { - elem->changed = true; - tag_changed = true; - } - } - break; - case NEU_TYPE_DOUBLE: - if (elem->value.precision == 0) { - elem->changed = elem->value.value.d64 != value.value.d64; - tag_changed = elem->changed; - } else { - if (fabs(elem->value.value.d64 - value.value.d64) > - pow(0.1, elem->value.precision)) { - elem->changed = true; - tag_changed = true; - } - } - - break; - case NEU_TYPE_ERROR: - elem->changed = true; - tag_changed = true; - break; - } - } - - if (sub_filter_err && value.type != NEU_TYPE_ERROR) { - elem->value_old.type = value.type; - elem->value_old.value = value.value; - elem->value_old.precision = value.precision; + if (elem == NULL) { + ret = NEU_ERR_PLUGIN_TAG_NOT_READY; + neu_free_dvalue(&value); + for (int i = 0; metas && i < n_meta; i++) { + neu_free_dvalue(&metas[i].value); } + goto done; + } - error_not_report: - - if (change) { - elem->changed = true; + bool resolved = false; + if (elem->configured && elem->config_type == NEU_TYPE_AUTO && + value.type != NEU_TYPE_ERROR) { + neu_type_e type = semantic_type(&value); + ret = sample_validate(elem, &value); + if (ret != NEU_ERR_SUCCESS) { + neu_free_dvalue(&value); + memset(&value, 0, sizeof(value)); + value.type = NEU_TYPE_ERROR; + value.value.i32 = ret; + } else if (elem->resolved_type == NEU_TYPE_AUTO) { + elem->resolved_type = type; + resolved = true; } + } + if (elem->configured) { + value.precision = elem->precision; + } else { + value.precision = elem->value.precision; + } - if (value.type == NEU_TYPE_PTR) { - elem->value.value.ptr.length = value.value.ptr.length; - elem->value.value.ptr.type = value.value.ptr.type; - if (elem->value.value.ptr.ptr != NULL) { - free(elem->value.value.ptr.ptr); - } - elem->value.value.ptr.ptr = calloc(1, value.value.ptr.length); - memcpy(elem->value.value.ptr.ptr, value.value.ptr.ptr, - value.value.ptr.length); - } else if (value.type == NEU_TYPE_CUSTOM) { - if (elem->value.type == NEU_TYPE_CUSTOM) { - if (elem->value.value.json != NULL) { - json_decref(elem->value.value.json); - elem->value.value.json = NULL; - } - } - - elem->value.value.json = value.value.json; - - } else if (value.type == NEU_TYPE_ARRAY_STRING) { - if (elem->value.type == NEU_TYPE_ARRAY_STRING) { - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - free(elem->value.value.strs.strs[i]); - elem->value.value.strs.strs[i] = NULL; - } - free(elem->value.value.strs.strs); - } - elem->value.value.strs.length = value.value.strs.length; - elem->value.value.strs.strs = value.value.strs.strs; - - } else if (NEU_TYPE_ARRAY_CHAR < value.type && - value.type < NEU_TYPE_ARRAY_STRING) { - if (NEU_TYPE_ARRAY_CHAR < elem->value.type && - elem->value.type < NEU_TYPE_ARRAY_STRING) { - free(elem->value.value.bools.bools); - } - elem->value.value.bools.length = value.value.bools.length; - elem->value.value.bools.bools = value.value.bools.bools; - } else if (value.type == NEU_TYPE_ERROR) { - if (elem->value.type == NEU_TYPE_CUSTOM) { - if (elem->value.value.json != NULL) { - json_decref(elem->value.value.json); - elem->value.value.json = NULL; - } - } - - if (elem->value.type == NEU_TYPE_PTR) { - if (elem->value.value.ptr.ptr != NULL) { - free(elem->value.value.ptr.ptr); - elem->value.value.ptr.ptr = NULL; - } - } - - if (elem->value.type == NEU_TYPE_ARRAY_STRING) { - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - free(elem->value.value.strs.strs[i]); - elem->value.value.strs.strs[i] = NULL; - } - free(elem->value.value.strs.strs); - } - - if (NEU_TYPE_ARRAY_CHAR < elem->value.type && - elem->value.type < NEU_TYPE_ARRAY_STRING) { - free(elem->value.value.bools.bools); - elem->value.value.bools.bools = NULL; - } - - elem->value.value = value.value; - } else { - elem->value.value = value.value; + elem->timestamp = timestamp; + if (!(sub_filter_err && value.type == NEU_TYPE_ERROR)) { + const neu_dvalue_t *previous = &elem->value; + bool has_previous = true; + if (sub_filter_err && elem->value.type == NEU_TYPE_ERROR) { + previous = &elem->value_old; + has_previous = elem->has_old; } - elem->value.type = value.type; + tag_changed = resolved || !has_previous || + !value_equal(previous, &value) || + previous->attrs.has_quality != value.attrs.has_quality || + (value.attrs.has_quality && + previous->attrs.quality != value.attrs.quality); + elem->changed |= tag_changed; + } + if (sub_filter_err && value.type != NEU_TYPE_ERROR) { + value_free(&elem->value_old); + value_copy(&value, &elem->value_old); + elem->has_old = true; + } + elem->changed |= force; - if (metas != NULL && n_meta != 0) { - if (elem->metas != NULL) { - free(elem->metas); - } - elem->metas = calloc(n_meta, sizeof(neu_tag_meta_t)); - for (int i = 0; i < n_meta; i++) { - if (!neu_dvalue_is_array(&metas[i].value)) { - memcpy(&elem->metas[i], &metas[i], sizeof(neu_tag_meta_t)); - } else { - neu_free_dvalue(&metas[i].value); - elem->metas[i].value.type = NEU_TYPE_INT8; - } + neu_dvalue_t stored = value; + if (value.type == NEU_TYPE_PTR) { + value_copy(&value, &stored); + } + value_free(&elem->value); + elem->value = stored; + + if (metas != NULL && n_meta > 0) { + metas_free(elem->metas, elem->n_meta); + elem->metas = calloc(n_meta, sizeof(neu_tag_meta_t)); + elem->n_meta = n_meta; + for (int i = 0; i < n_meta; i++) { + if (!neu_dvalue_is_array(&metas[i].value)) { + elem->metas[i] = metas[i]; + if (metas[i].value.type == NEU_TYPE_PTR) { + value_copy(&metas[i].value, &elem->metas[i].value); + } + } else { + neu_free_dvalue(&metas[i].value); + elem->metas[i].value.type = NEU_TYPE_INT8; } - - elem->n_meta = n_meta; } } +done: pthread_mutex_unlock(&cache->mtx); - return tag_changed; + if (changed) { + *changed = tag_changed; + } + return ret; +} + +bool neu_driver_cache_update_change(neu_driver_cache_t *cache, + const char *group, const char *tag, + int64_t timestamp, neu_dvalue_t value, + neu_tag_meta_t *metas, int n_meta, + bool change) +{ + bool changed = false; + neu_driver_cache_update_checked(cache, group, tag, timestamp, value, metas, + n_meta, change, &changed); + return changed; } void neu_driver_cache_update(neu_driver_cache_t *cache, const char *group, @@ -886,159 +549,23 @@ int neu_driver_cache_meta_get(neu_driver_cache_t *cache, const char *group, HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem); if (elem != NULL) { - value->timestamp = elem->timestamp; - value->value.type = elem->value.type; - value->value.precision = elem->value.precision; + value->resolved_type = elem->resolved_type; + value->generation = elem->generation; + value->timestamp = elem->timestamp; + value_copy(&elem->value, &value->value); // assert(n_meta <= NEU_TAG_META_SIZE); if (elem->metas) { *metas = calloc(elem->n_meta, sizeof(neu_tag_meta_t)); - memcpy(*metas, elem->metas, sizeof(neu_tag_meta_t) * elem->n_meta); + for (int i = 0; i < elem->n_meta; i++) { + (*metas)[i] = elem->metas[i]; + value_copy(&elem->metas[i].value, &(*metas)[i].value); + } } else { *metas = NULL; } *n_meta = elem->n_meta; - switch (elem->value.type) { - case NEU_TYPE_INT8: - case NEU_TYPE_UINT8: - case NEU_TYPE_BIT: - value->value.value.u8 = elem->value.value.u8; - break; - case NEU_TYPE_INT16: - case NEU_TYPE_UINT16: - case NEU_TYPE_WORD: - value->value.value.u16 = elem->value.value.u16; - break; - case NEU_TYPE_INT32: - case NEU_TYPE_UINT32: - case NEU_TYPE_DWORD: - case NEU_TYPE_FLOAT: - case NEU_TYPE_ERROR: - value->value.value.u32 = elem->value.value.u32; - break; - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - case NEU_TYPE_DOUBLE: - case NEU_TYPE_LWORD: - value->value.value.u64 = elem->value.value.u64; - break; - case NEU_TYPE_BOOL: - value->value.value.boolean = elem->value.value.boolean; - break; - case NEU_TYPE_STRING: - case NEU_TYPE_TIME: - case NEU_TYPE_DATA_AND_TIME: - case NEU_TYPE_ARRAY_CHAR: - memcpy(value->value.value.str, elem->value.value.str, - sizeof(elem->value.value.str)); - break; - case NEU_TYPE_BYTES: - value->value.value.bytes.length = elem->value.value.bytes.length; - memcpy(value->value.value.bytes.bytes, - elem->value.value.bytes.bytes, - elem->value.value.bytes.length); - break; - case NEU_TYPE_ARRAY_BOOL: - value->value.value.bools.length = elem->value.value.bools.length; - value->value.value.bools.bools = - calloc(elem->value.value.bools.length, sizeof(bool)); - memcpy(value->value.value.bools.bools, - elem->value.value.bools.bools, - elem->value.value.bools.length); - break; - case NEU_TYPE_ARRAY_INT8: - value->value.value.i8s.length = elem->value.value.i8s.length; - value->value.value.i8s.i8s = - calloc(elem->value.value.i8s.length, sizeof(int8_t)); - memcpy(value->value.value.i8s.i8s, elem->value.value.i8s.i8s, - elem->value.value.i8s.length); - break; - case NEU_TYPE_ARRAY_UINT8: - value->value.value.u8s.length = elem->value.value.u8s.length; - value->value.value.u8s.u8s = - calloc(elem->value.value.u8s.length, sizeof(uint8_t)); - memcpy(value->value.value.u8s.u8s, elem->value.value.u8s.u8s, - elem->value.value.u8s.length); - break; - case NEU_TYPE_ARRAY_INT16: - value->value.value.i16s.length = elem->value.value.i16s.length; - value->value.value.i16s.i16s = - calloc(elem->value.value.i16s.length, sizeof(int16_t)); - memcpy(value->value.value.i16s.i16s, elem->value.value.i16s.i16s, - elem->value.value.i16s.length * sizeof(int16_t)); - break; - case NEU_TYPE_ARRAY_UINT16: - value->value.value.u16s.length = elem->value.value.u16s.length; - value->value.value.u16s.u16s = - calloc(elem->value.value.u16s.length, sizeof(uint16_t)); - memcpy(value->value.value.u16s.u16s, elem->value.value.u16s.u16s, - elem->value.value.u16s.length * sizeof(uint16_t)); - break; - case NEU_TYPE_ARRAY_INT32: - value->value.value.i32s.length = elem->value.value.i32s.length; - value->value.value.i32s.i32s = - calloc(elem->value.value.i32s.length, sizeof(int32_t)); - memcpy(value->value.value.i32s.i32s, elem->value.value.i32s.i32s, - elem->value.value.i32s.length * sizeof(int32_t)); - break; - case NEU_TYPE_ARRAY_UINT32: - value->value.value.u32s.length = elem->value.value.u32s.length; - value->value.value.u32s.u32s = - calloc(elem->value.value.u32s.length, sizeof(uint32_t)); - memcpy(value->value.value.u32s.u32s, elem->value.value.u32s.u32s, - elem->value.value.u32s.length * sizeof(uint32_t)); - break; - case NEU_TYPE_ARRAY_INT64: - value->value.value.i64s.length = elem->value.value.i64s.length; - value->value.value.i64s.i64s = - calloc(elem->value.value.i64s.length, sizeof(int64_t)); - memcpy(value->value.value.i64s.i64s, elem->value.value.i64s.i64s, - elem->value.value.i64s.length * sizeof(int64_t)); - break; - case NEU_TYPE_ARRAY_UINT64: - value->value.value.u64s.length = elem->value.value.u64s.length; - value->value.value.u64s.u64s = - calloc(elem->value.value.u64s.length, sizeof(uint64_t)); - memcpy(value->value.value.u64s.u64s, elem->value.value.u64s.u64s, - elem->value.value.u64s.length * sizeof(uint64_t)); - break; - case NEU_TYPE_ARRAY_FLOAT: - value->value.value.f32s.length = elem->value.value.f32s.length; - value->value.value.f32s.f32s = - calloc(elem->value.value.f32s.length, sizeof(float)); - memcpy(value->value.value.f32s.f32s, elem->value.value.f32s.f32s, - elem->value.value.f32s.length * sizeof(float)); - break; - case NEU_TYPE_ARRAY_DOUBLE: - value->value.value.f64s.length = elem->value.value.f64s.length; - value->value.value.f64s.f64s = - calloc(elem->value.value.f64s.length, sizeof(double)); - memcpy(value->value.value.f64s.f64s, elem->value.value.f64s.f64s, - elem->value.value.f64s.length * sizeof(double)); - break; - case NEU_TYPE_ARRAY_STRING: - value->value.value.strs.length = elem->value.value.strs.length; - value->value.value.strs.strs = - calloc(elem->value.value.strs.length, sizeof(char *)); - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - value->value.value.strs.strs[i] = - strdup(elem->value.value.strs.strs[i]); - } - break; - case NEU_TYPE_PTR: - value->value.value.ptr.length = elem->value.value.ptr.length; - value->value.value.ptr.type = elem->value.value.ptr.type; - value->value.value.ptr.ptr = - calloc(1, elem->value.value.ptr.length); - memcpy(value->value.value.ptr.ptr, elem->value.value.ptr.ptr, - elem->value.value.ptr.length); - break; - case NEU_TYPE_CUSTOM: - value->value.value.json = json_deep_copy(elem->value.value.json); - break; - } - ret = 0; } @@ -1060,159 +587,23 @@ int neu_driver_cache_meta_get_changed(neu_driver_cache_t *cache, HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem); if (elem != NULL && elem->changed) { - value->timestamp = elem->timestamp; - value->value.type = elem->value.type; - value->value.precision = elem->value.precision; + value->resolved_type = elem->resolved_type; + value->generation = elem->generation; + value->timestamp = elem->timestamp; + value_copy(&elem->value, &value->value); // assert(n_meta <= NEU_TAG_META_SIZE); if (elem->metas) { *metas = calloc(elem->n_meta, sizeof(neu_tag_meta_t)); - memcpy(*metas, elem->metas, sizeof(neu_tag_meta_t) * elem->n_meta); + for (int i = 0; i < elem->n_meta; i++) { + (*metas)[i] = elem->metas[i]; + value_copy(&elem->metas[i].value, &(*metas)[i].value); + } } else { *metas = NULL; } *n_meta = elem->n_meta; - switch (elem->value.type) { - case NEU_TYPE_INT8: - case NEU_TYPE_UINT8: - case NEU_TYPE_BIT: - value->value.value.u8 = elem->value.value.u8; - break; - case NEU_TYPE_INT16: - case NEU_TYPE_UINT16: - case NEU_TYPE_WORD: - value->value.value.u16 = elem->value.value.u16; - break; - case NEU_TYPE_INT32: - case NEU_TYPE_UINT32: - case NEU_TYPE_FLOAT: - case NEU_TYPE_ERROR: - case NEU_TYPE_DWORD: - value->value.value.u32 = elem->value.value.u32; - break; - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - case NEU_TYPE_DOUBLE: - case NEU_TYPE_LWORD: - value->value.value.u64 = elem->value.value.u64; - break; - case NEU_TYPE_BOOL: - value->value.value.boolean = elem->value.value.boolean; - break; - case NEU_TYPE_STRING: - case NEU_TYPE_TIME: - case NEU_TYPE_DATA_AND_TIME: - case NEU_TYPE_ARRAY_CHAR: - memcpy(value->value.value.str, elem->value.value.str, - sizeof(elem->value.value.str)); - break; - case NEU_TYPE_BYTES: - value->value.value.bytes.length = elem->value.value.bytes.length; - memcpy(value->value.value.bytes.bytes, - elem->value.value.bytes.bytes, - elem->value.value.bytes.length); - break; - case NEU_TYPE_ARRAY_BOOL: - value->value.value.bools.length = elem->value.value.bools.length; - value->value.value.bools.bools = - calloc(elem->value.value.bools.length, sizeof(bool)); - memcpy(value->value.value.bools.bools, - elem->value.value.bools.bools, - elem->value.value.bools.length); - break; - case NEU_TYPE_ARRAY_INT8: - value->value.value.i8s.length = elem->value.value.i8s.length; - value->value.value.i8s.i8s = - calloc(elem->value.value.i8s.length, sizeof(int8_t)); - memcpy(value->value.value.i8s.i8s, elem->value.value.i8s.i8s, - elem->value.value.i8s.length); - break; - case NEU_TYPE_ARRAY_UINT8: - value->value.value.u8s.length = elem->value.value.u8s.length; - value->value.value.u8s.u8s = - calloc(elem->value.value.u8s.length, sizeof(uint8_t)); - memcpy(value->value.value.u8s.u8s, elem->value.value.u8s.u8s, - elem->value.value.u8s.length); - break; - case NEU_TYPE_ARRAY_INT16: - value->value.value.i16s.length = elem->value.value.i16s.length; - value->value.value.i16s.i16s = - calloc(elem->value.value.i16s.length, sizeof(int16_t)); - memcpy(value->value.value.i16s.i16s, elem->value.value.i16s.i16s, - elem->value.value.i16s.length * sizeof(int16_t)); - break; - case NEU_TYPE_ARRAY_UINT16: - value->value.value.u16s.length = elem->value.value.u16s.length; - value->value.value.u16s.u16s = - calloc(elem->value.value.u16s.length, sizeof(uint16_t)); - memcpy(value->value.value.u16s.u16s, elem->value.value.u16s.u16s, - elem->value.value.u16s.length * sizeof(uint16_t)); - break; - case NEU_TYPE_ARRAY_INT32: - value->value.value.i32s.length = elem->value.value.i32s.length; - value->value.value.i32s.i32s = - calloc(elem->value.value.i32s.length, sizeof(int32_t)); - memcpy(value->value.value.i32s.i32s, elem->value.value.i32s.i32s, - elem->value.value.i32s.length * sizeof(int32_t)); - break; - case NEU_TYPE_ARRAY_UINT32: - value->value.value.u32s.length = elem->value.value.u32s.length; - value->value.value.u32s.u32s = - calloc(elem->value.value.u32s.length, sizeof(uint32_t)); - memcpy(value->value.value.u32s.u32s, elem->value.value.u32s.u32s, - elem->value.value.u32s.length * sizeof(uint32_t)); - break; - case NEU_TYPE_ARRAY_INT64: - value->value.value.i64s.length = elem->value.value.i64s.length; - value->value.value.i64s.i64s = - calloc(elem->value.value.i64s.length, sizeof(int64_t)); - memcpy(value->value.value.i64s.i64s, elem->value.value.i64s.i64s, - elem->value.value.i64s.length * sizeof(int64_t)); - break; - case NEU_TYPE_ARRAY_UINT64: - value->value.value.u64s.length = elem->value.value.u64s.length; - value->value.value.u64s.u64s = - calloc(elem->value.value.u64s.length, sizeof(uint64_t)); - memcpy(value->value.value.u64s.u64s, elem->value.value.u64s.u64s, - elem->value.value.u64s.length * sizeof(uint64_t)); - break; - case NEU_TYPE_ARRAY_FLOAT: - value->value.value.f32s.length = elem->value.value.f32s.length; - value->value.value.f32s.f32s = - calloc(elem->value.value.f32s.length, sizeof(float)); - memcpy(value->value.value.f32s.f32s, elem->value.value.f32s.f32s, - elem->value.value.f32s.length * sizeof(float)); - break; - case NEU_TYPE_ARRAY_DOUBLE: - value->value.value.f64s.length = elem->value.value.f64s.length; - value->value.value.f64s.f64s = - calloc(elem->value.value.f64s.length, sizeof(double)); - memcpy(value->value.value.f64s.f64s, elem->value.value.f64s.f64s, - elem->value.value.f64s.length * sizeof(double)); - break; - case NEU_TYPE_ARRAY_STRING: - value->value.value.strs.length = elem->value.value.strs.length; - value->value.value.strs.strs = - calloc(elem->value.value.strs.length, sizeof(char *)); - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - value->value.value.strs.strs[i] = - strdup(elem->value.value.strs.strs[i]); - } - break; - case NEU_TYPE_PTR: - value->value.value.ptr.length = elem->value.value.ptr.length; - value->value.value.ptr.type = elem->value.value.ptr.type; - value->value.value.ptr.ptr = - calloc(1, elem->value.value.ptr.length); - memcpy(value->value.value.ptr.ptr, elem->value.value.ptr.ptr, - elem->value.value.ptr.length); - break; - case NEU_TYPE_CUSTOM: - value->value.value.json = json_deep_copy(elem->value.value.json); - break; - } - if (elem->value.type != NEU_TYPE_ERROR) { elem->changed = false; } @@ -1235,32 +626,7 @@ void neu_driver_cache_del(neu_driver_cache_t *cache, const char *group, if (elem != NULL) { HASH_DEL(cache->table, elem); - if (elem->value.type == NEU_TYPE_PTR) { - if (elem->value.value.ptr.ptr != NULL) { - free(elem->value.value.ptr.ptr); - elem->value.value.ptr.ptr = NULL; - } - } else if (elem->value.type == NEU_TYPE_CUSTOM) { - if (elem->value.value.json != NULL) { - json_decref(elem->value.value.json); - elem->value.value.json = NULL; - } - } else if (elem->value.type == NEU_TYPE_ARRAY_STRING) { - for (size_t i = 0; i < elem->value.value.strs.length; i++) { - free(elem->value.value.strs.strs[i]); - elem->value.value.strs.strs[i] = NULL; - } - free(elem->value.value.strs.strs); - } else if (NEU_TYPE_ARRAY_CHAR < elem->value.type && - elem->value.type < NEU_TYPE_ARRAY_STRING) { - free(elem->value.value.bools.bools); - } - if (elem->metas != NULL) { - for (int i = 0; i < elem->n_meta; i++) { - neu_free_dvalue(&elem->metas[i].value); - } - free(elem->metas); - } + elem_clear(elem); free(elem); } diff --git a/src/adapter/driver/cache.h b/src/adapter/driver/cache.h index dc7ff6147..66da7ec7d 100644 --- a/src/adapter/driver/cache.h +++ b/src/adapter/driver/cache.h @@ -22,7 +22,7 @@ #include -#include "type.h" +#include "tag.h" typedef struct neu_driver_cache neu_driver_cache_t; @@ -31,6 +31,16 @@ void neu_driver_cache_destroy(neu_driver_cache_t *cache); void neu_driver_cache_add(neu_driver_cache_t *cache, const char *group, const char *tag, neu_dvalue_t value); +void neu_driver_cache_add_tag(neu_driver_cache_t *cache, const char *group, + const neu_datatag_t *tag, neu_dvalue_t initial); +int neu_driver_cache_update_checked(neu_driver_cache_t *cache, + const char *group, const char *tag, + int64_t timestamp, neu_dvalue_t value, + neu_tag_meta_t *metas, int n_meta, + bool force, bool *changed); +int neu_driver_cache_type_get(neu_driver_cache_t *cache, const char *group, + const char *tag, neu_type_e *type, + uint64_t *generation); void neu_driver_cache_update(neu_driver_cache_t *cache, const char *group, const char *tag, int64_t timestamp, neu_dvalue_t value, neu_tag_meta_t *metas, @@ -54,6 +64,8 @@ void *neu_driver_cache_get_trace(neu_driver_cache_t *cache, const char *group); typedef struct { neu_dvalue_t value; int64_t timestamp; + neu_type_e resolved_type; + uint64_t generation; } neu_driver_cache_value_t; int neu_driver_cache_meta_get(neu_driver_cache_t *cache, const char *group, diff --git a/src/adapter/driver/driver.c b/src/adapter/driver/driver.c index 7444c6e40..b31dcf994 100644 --- a/src/adapter/driver/driver.c +++ b/src/adapter/driver/driver.c @@ -43,11 +43,19 @@ #include "core/node_manager.h" #include "otel/otel_manager.h" +typedef struct { + char group[NEU_GROUP_NAME_LEN]; + char tag[NEU_TAG_NAME_LEN]; + neu_type_e type; + uint64_t generation; +} write_type_snapshot_t; + typedef struct to_be_write_tag { bool single; neu_datatag_t *tag; neu_value_u value; UT_array * tvs; + UT_array * types; void * req; } to_be_write_tag_t; @@ -137,9 +145,24 @@ static inline void start_group_timer(neu_adapter_driver_t *driver, group_t * grp); static inline void stop_group_timer(neu_adapter_driver_t *driver, group_t *grp); +static neu_datatag_t effective_tag(const neu_datatag_t *tag, neu_type_e type) +{ + neu_datatag_t effective = *tag; + if (tag->type == NEU_TYPE_AUTO) { + effective.type = type; + memset(&effective.option, 0, sizeof(effective.option)); + neu_datatag_parse_addr_option(&effective, &effective.option); + } + return effective; +} + static void format_tag_value(neu_dvalue_t *value) { double scale = pow(10, 5); + if (!isfinite(value->value.d64) || + fabs(value->value.d64) >= (double) UINT64_MAX / scale) { + return; + } int negative = 1; @@ -417,11 +440,14 @@ static void update_with_meta(neu_adapter_t *adapter, const char *group, utarray_free(tags); } } else { - neu_driver_cache_update(driver->cache, group, tag, global_timestamp, - value, metas, n_meta); + bool changed = false; + int error = neu_driver_cache_update_checked( + driver->cache, group, tag, global_timestamp, value, metas, n_meta, + false, &changed); update_metric(&driver->adapter, NEU_METRIC_TAG_READS_TOTAL, 1, NULL); update_metric(&driver->adapter, NEU_METRIC_TAG_READ_ERRORS_TOTAL, - NEU_TYPE_ERROR == value.type, NULL); + NEU_TYPE_ERROR == value.type || error != NEU_ERR_SUCCESS, + NULL); } nlog_debug( "update driver: %s, group: %s, tag: %s, type: %s, timestamp: %" PRId64 @@ -463,12 +489,13 @@ static void update_im_f_m(neu_adapter_t *adapter, const char *group, return; } - bool changed = neu_driver_cache_update_change(driver->cache, group, tag, - global_timestamp, value, - metas, n_meta, false); + bool changed = false; + int error = neu_driver_cache_update_checked(driver->cache, group, tag, + global_timestamp, value, metas, + n_meta, false, &changed); driver->adapter.cb_funs.update_metric(&driver->adapter, NEU_METRIC_TAG_READS_TOTAL, 1, NULL); - if (value.type == NEU_TYPE_ERROR) { + if (value.type == NEU_TYPE_ERROR || error != NEU_ERR_SUCCESS) { return; } @@ -563,11 +590,13 @@ static void update_im(neu_adapter_t *adapter, const char *group, return; } - neu_driver_cache_update_change(driver->cache, group, tag, global_timestamp, - value, metas, n_meta, true); + bool changed = false; + int error = neu_driver_cache_update_checked(driver->cache, group, tag, + global_timestamp, value, metas, + n_meta, true, &changed); driver->adapter.cb_funs.update_metric(&driver->adapter, NEU_METRIC_TAG_READS_TOTAL, 1, NULL); - if (value.type == NEU_TYPE_ERROR) { + if (value.type == NEU_TYPE_ERROR || error != NEU_ERR_SUCCESS) { return; } @@ -729,6 +758,9 @@ int neu_adapter_driver_uninit(neu_adapter_driver_t *driver) utarray_foreach(el->wt_tags, to_be_write_tag_t *, tag) { + if (tag->types != NULL) { + utarray_free(tag->types); + } neu_tag_free(tag->tag); } @@ -1254,77 +1286,34 @@ static void fix_value(neu_datatag_t *tag, neu_type_e value_type, } static void cal_decimal(neu_type_e tag_type, neu_type_e value_type, - neu_value_u *value, double decimal) + neu_value_u *value, double decimal, double bias) { + long double result = value_type == NEU_TYPE_INT64 + ? (long double) value->i64 + : (long double) value->d64; + result -= (long double) bias; + if (decimal != 0) { + result /= (long double) decimal; + } switch (tag_type) { case NEU_TYPE_INT8: - if (value_type == NEU_TYPE_DOUBLE) { - value->i8 = (int8_t) round(value->d64 / decimal); - } else { - value->i8 = (int8_t) round(value->i64 / decimal); - } - break; case NEU_TYPE_INT16: - if (value_type == NEU_TYPE_DOUBLE) { - value->i16 = (int16_t) round(value->d64 / decimal); - } else { - value->i16 = (int16_t) round(value->i64 / decimal); - } - break; case NEU_TYPE_INT32: - if (value_type == NEU_TYPE_DOUBLE) { - value->i32 = (int32_t) round(value->d64 / decimal); - } else { - value->i32 = (int32_t) round(value->i64 / decimal); - } - break; case NEU_TYPE_INT64: - if (value_type == NEU_TYPE_DOUBLE) { - value->i64 = (int64_t) round(value->d64 / decimal); - } else { - value->i64 = (int64_t) round(value->i64 / decimal); - } + value->i64 = (int64_t) roundl(result); break; case NEU_TYPE_UINT8: - if (value_type == NEU_TYPE_DOUBLE) { - value->u8 = (uint8_t) round(value->d64 / decimal); - } else { - value->u8 = (uint8_t) round(value->u64 / decimal); - } - break; case NEU_TYPE_UINT16: case NEU_TYPE_WORD: - if (value_type == NEU_TYPE_DOUBLE) { - value->u16 = (uint16_t) round(value->d64 / decimal); - } else { - value->u16 = (uint16_t) round(value->u64 / decimal); - } - break; case NEU_TYPE_UINT32: - case NEU_TYPE_LWORD: - if (value_type == NEU_TYPE_DOUBLE) { - value->u32 = (uint32_t) round(value->d64 / decimal); - } else { - value->u32 = (uint32_t) round(value->u64 / decimal); - } - break; - case NEU_TYPE_UINT64: case NEU_TYPE_DWORD: - if (value_type == NEU_TYPE_DOUBLE) { - value->u64 = (uint64_t) round(value->d64 / decimal); - } else { - value->u64 = (uint64_t) round(value->u64 / decimal); - } + case NEU_TYPE_UINT64: + case NEU_TYPE_LWORD: + value->u64 = (uint64_t) roundl(result); break; case NEU_TYPE_FLOAT: case NEU_TYPE_DOUBLE: - if (value_type == NEU_TYPE_INT64) { - value->d64 = (double) (value->d64 / decimal); - } else if (value_type == NEU_TYPE_DOUBLE) { - value->d64 = value->d64 / decimal; - } else if (value_type == NEU_TYPE_FLOAT) { - value->f32 = value->f32 / decimal; - } + value->d64 = (double) result; break; default: break; @@ -1363,156 +1352,239 @@ int check_value(neu_type_e write_type, int64_t value, int64_t value_min, } } -int is_value_in_range(neu_type_e tag_type, int64_t value, double value_d, - neu_type_e write_type, double decimal) +static int check_write_value(neu_type_e tag_type, int64_t value, double value_d, + neu_type_e write_type, double decimal, double bias) { - if (decimal != 0) { - double result; - - if (write_type == NEU_TYPE_INT64) { - result = value / decimal; - } else { - result = value_d / decimal; + long double min = 0, max = 0; + bool integer = true; + switch (tag_type) { + case NEU_TYPE_AUTO: + return NEU_ERR_PLUGIN_TAG_NOT_READY; + case NEU_TYPE_INT8: + min = INT8_MIN; + max = INT8_MAX; + break; + case NEU_TYPE_UINT8: + max = UINT8_MAX; + break; + case NEU_TYPE_INT16: + min = INT16_MIN; + max = INT16_MAX; + break; + case NEU_TYPE_UINT16: + case NEU_TYPE_WORD: + max = UINT16_MAX; + break; + case NEU_TYPE_INT32: + min = INT32_MIN; + max = INT32_MAX; + break; + case NEU_TYPE_UINT32: + case NEU_TYPE_DWORD: + max = UINT32_MAX; + break; + case NEU_TYPE_INT64: + min = INT64_MIN; + max = INT64_MAX; + break; + case NEU_TYPE_UINT64: + case NEU_TYPE_LWORD: + max = UINT64_MAX; + break; + case NEU_TYPE_BIT: + max = 1; + break; + case NEU_TYPE_FLOAT: + min = -FLT_MAX; + max = FLT_MAX; + integer = false; + break; + case NEU_TYPE_DOUBLE: + min = -DBL_MAX; + max = DBL_MAX; + integer = false; + break; + default: { + if (bias != 0) { + return NEU_ERR_TAG_BIAS_INVALID; } - - switch (tag_type) { - case NEU_TYPE_INT8: - return check_value_decimal(write_type, result, INT8_MIN, INT8_MAX); - case NEU_TYPE_UINT8: - return check_value_decimal(write_type, result, 0, UINT8_MAX); - case NEU_TYPE_INT16: - return check_value_decimal(write_type, result, INT16_MIN, - INT16_MAX); - case NEU_TYPE_UINT16: - return check_value_decimal(write_type, result, 0, UINT16_MAX); - case NEU_TYPE_INT32: - return check_value_decimal(write_type, result, INT32_MIN, - INT32_MAX); - case NEU_TYPE_UINT32: - return check_value_decimal(write_type, result, 0, UINT32_MAX); - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - if (write_type != NEU_TYPE_INT64 && write_type != NEU_TYPE_DOUBLE) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else if (almost_equal(ceil(result), result, EPSILON) || - almost_equal(floor(result), result, EPSILON)) { - return NEU_ERR_SUCCESS; - } else { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - case NEU_TYPE_FLOAT: - if (write_type != NEU_TYPE_DOUBLE && write_type != NEU_TYPE_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - if (result < (int64_t) -FLT_MAX || result > (int64_t) FLT_MAX) { - return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; - } else { - return NEU_ERR_SUCCESS; - } - case NEU_TYPE_DOUBLE: - if (write_type != NEU_TYPE_DOUBLE && write_type != NEU_TYPE_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - return NEU_ERR_SUCCESS; - case NEU_TYPE_STRING: - case NEU_TYPE_BOOL: - case NEU_TYPE_BIT: + if (decimal != 0) { return NEU_ERR_TAG_DECIMAL_INVALID; - default: - return NEU_ERR_SUCCESS; } - } else { + bool valid = false; switch (tag_type) { case NEU_TYPE_BOOL: - if (write_type == NEU_TYPE_BOOL) { - return NEU_ERR_SUCCESS; - } else { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - case NEU_TYPE_BIT: - if (write_type != NEU_TYPE_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else if (value != 1 && value != 0) { - return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; - } else { - return NEU_ERR_SUCCESS; - } - case NEU_TYPE_INT8: - return check_value(write_type, value, INT8_MIN, INT8_MAX); - case NEU_TYPE_UINT8: - return check_value(write_type, value, 0, UINT8_MAX); - case NEU_TYPE_INT16: - return check_value(write_type, value, INT16_MIN, INT16_MAX); - case NEU_TYPE_UINT16: - return check_value(write_type, value, 0, UINT16_MAX); - case NEU_TYPE_INT32: - return check_value(write_type, value, INT32_MIN, INT32_MAX); - case NEU_TYPE_UINT32: - return check_value(write_type, value, 0, UINT32_MAX); - case NEU_TYPE_INT64: - case NEU_TYPE_UINT64: - if (write_type == NEU_TYPE_INT64) { - return NEU_ERR_SUCCESS; - } else { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - case NEU_TYPE_FLOAT: - if (write_type != NEU_TYPE_DOUBLE && write_type != NEU_TYPE_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - if (value < (int64_t) -FLT_MAX || value > (int64_t) FLT_MAX) { - return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; - } else { - return NEU_ERR_SUCCESS; - } - case NEU_TYPE_DOUBLE: - if (write_type != NEU_TYPE_DOUBLE && write_type != NEU_TYPE_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } - return NEU_ERR_SUCCESS; + valid = write_type == NEU_TYPE_BOOL; + break; case NEU_TYPE_STRING: - if (write_type != NEU_TYPE_STRING) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else { - return NEU_ERR_SUCCESS; - } + case NEU_TYPE_TIME: + case NEU_TYPE_DATA_AND_TIME: + case NEU_TYPE_ARRAY_CHAR: + valid = write_type == NEU_TYPE_STRING; + break; case NEU_TYPE_CUSTOM: - if (write_type != NEU_TYPE_CUSTOM) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else { - return NEU_ERR_SUCCESS; - } - case NEU_TYPE_ARRAY_UINT8: + valid = write_type == NEU_TYPE_CUSTOM; + break; + case NEU_TYPE_BYTES: case NEU_TYPE_ARRAY_INT8: - case NEU_TYPE_ARRAY_UINT16: + case NEU_TYPE_ARRAY_UINT8: case NEU_TYPE_ARRAY_INT16: - case NEU_TYPE_ARRAY_UINT32: + case NEU_TYPE_ARRAY_UINT16: case NEU_TYPE_ARRAY_INT32: - case NEU_TYPE_ARRAY_UINT64: + case NEU_TYPE_ARRAY_UINT32: case NEU_TYPE_ARRAY_INT64: - if (write_type != NEU_TYPE_ARRAY_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else { - return NEU_ERR_SUCCESS; - } + case NEU_TYPE_ARRAY_UINT64: + valid = write_type == NEU_TYPE_ARRAY_INT64; + break; case NEU_TYPE_ARRAY_FLOAT: case NEU_TYPE_ARRAY_DOUBLE: - if (write_type != NEU_TYPE_ARRAY_DOUBLE && - write_type != NEU_TYPE_ARRAY_INT64) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else { - return NEU_ERR_SUCCESS; - } + valid = write_type == NEU_TYPE_ARRAY_INT64 || + write_type == NEU_TYPE_ARRAY_DOUBLE; + break; + case NEU_TYPE_ARRAY_BOOL: + valid = write_type == NEU_TYPE_ARRAY_BOOL; + break; case NEU_TYPE_ARRAY_STRING: - if (write_type != NEU_TYPE_ARRAY_STRING) { - return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; - } else { - return NEU_ERR_SUCCESS; - } + valid = write_type == NEU_TYPE_ARRAY_STRING; + break; default: - return NEU_ERR_SUCCESS; + break; } + return valid ? NEU_ERR_SUCCESS : NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; } + } + if (write_type != NEU_TYPE_INT64 && + (write_type != NEU_TYPE_DOUBLE || + (integer && decimal == 0 && bias == 0))) { + return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; + } + if ((write_type == NEU_TYPE_DOUBLE && !isfinite(value_d)) || + (!isfinite(decimal) || !isfinite(bias))) { + return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; + } + long double result = write_type == NEU_TYPE_INT64 ? (long double) value + : (long double) value_d; + result -= (long double) bias; + if (decimal != 0) { + result /= (long double) decimal; + } + if (!isfinite(result) || result < min || result > max) { + return NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE; + } + if (integer && fabsl(result - roundl(result)) >= EPSILON) { + return NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH; + } + return NEU_ERR_SUCCESS; +} + +int is_value_in_range(neu_type_e tag_type, int64_t value, double value_d, + neu_type_e write_type, double decimal) +{ + return check_write_value(tag_type, value, value_d, write_type, decimal, 0); +} + +static int prepare_write_tag(neu_adapter_driver_t *driver, group_t *group, + neu_datatag_t *tag, neu_dvalue_t *value, + UT_array **types) +{ + if (!neu_tag_attribute_test(tag, NEU_ATTRIBUTE_WRITE)) { + return NEU_ERR_PLUGIN_TAG_NOT_ALLOW_WRITE; + } + if (tag->type == NEU_TYPE_AUTO) { + write_type_snapshot_t snapshot = { 0 }; + if (neu_group_is_change(group->group, group->timestamp) || + neu_driver_cache_type_get(driver->cache, group->name, tag->name, + &snapshot.type, &snapshot.generation) != + NEU_ERR_SUCCESS) { + return NEU_ERR_PLUGIN_TAG_NOT_READY; + } + snprintf(snapshot.group, sizeof(snapshot.group), "%s", group->name); + snprintf(snapshot.tag, sizeof(snapshot.tag), "%s", tag->name); + *tag = effective_tag(tag, snapshot.type); + if (*types == NULL) { + UT_icd icd = { sizeof(write_type_snapshot_t), NULL, NULL, NULL }; + utarray_new(*types, &icd); + } + utarray_push_back(*types, &snapshot); + } + int ret = check_write_value(tag->type, value->value.i64, value->value.d64, + value->type, tag->decimal, tag->bias); + if (ret != NEU_ERR_SUCCESS) { + return ret; + } + if (value->type == NEU_TYPE_ARRAY_INT64 || + value->type == NEU_TYPE_ARRAY_DOUBLE) { + neu_type_e element = NEU_TYPE_AUTO; + if (tag->type >= NEU_TYPE_ARRAY_INT8 && + tag->type <= NEU_TYPE_ARRAY_DOUBLE) { + element = NEU_TYPE_INT8 + tag->type - NEU_TYPE_ARRAY_INT8; + } else if (tag->type == NEU_TYPE_BYTES) { + element = NEU_TYPE_UINT8; + } + if (element != NEU_TYPE_AUTO) { + uint32_t length = value->type == NEU_TYPE_ARRAY_INT64 + ? value->value.i64s.length + : value->value.f64s.length; + for (uint32_t i = 0; i < length; ++i) { + ret = is_value_in_range(element, + value->type == NEU_TYPE_ARRAY_INT64 + ? value->value.i64s.i64s[i] + : 0, + value->type == NEU_TYPE_ARRAY_DOUBLE + ? value->value.f64s.f64s[i] + : 0, + value->type == NEU_TYPE_ARRAY_INT64 + ? NEU_TYPE_INT64 + : NEU_TYPE_DOUBLE, + 0); + if (ret != NEU_ERR_SUCCESS) { + return ret; + } + } + } + } + if (tag->decimal != 0 || tag->bias != 0) { + cal_decimal(tag->type, value->type, &value->value, tag->decimal, + tag->bias); + } else if ((tag->type == NEU_TYPE_FLOAT || tag->type == NEU_TYPE_DOUBLE) && + value->type == NEU_TYPE_INT64) { + value->value.d64 = (double) value->value.i64; + } + if (tag->type == NEU_TYPE_FLOAT || tag->type == NEU_TYPE_DOUBLE) { + value->type = NEU_TYPE_DOUBLE; + } + fix_value(tag, value->type, value); + value->type = tag->type; + return NEU_ERR_SUCCESS; +} + +static bool write_types_valid(neu_adapter_driver_t *driver, UT_array *types) +{ + if (types == NULL) { + return true; + } + utarray_foreach(types, write_type_snapshot_t *, snapshot) + { + group_t * group = find_group(driver, snapshot->group); + neu_type_e type = NEU_TYPE_AUTO; + uint64_t generation = 0; + if (group == NULL || + neu_group_is_change(group->group, group->timestamp) || + neu_driver_cache_type_get(driver->cache, snapshot->group, + snapshot->tag, &type, + &generation) != NEU_ERR_SUCCESS || + generation != snapshot->generation || type != snapshot->type) { + return false; + } + neu_datatag_t *tag = neu_group_find_tag(group->group, snapshot->tag); + bool valid = tag != NULL && tag->type == NEU_TYPE_AUTO && + neu_tag_attribute_test(tag, NEU_ATTRIBUTE_WRITE); + neu_tag_free(tag); + if (!valid) { + return false; + } + } + return true; } int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, @@ -1542,8 +1614,9 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, return NEU_ERR_GROUP_NOT_EXIST; } - UT_array *tags = NULL; - UT_icd icd = { sizeof(neu_plugin_tag_value_t), NULL, NULL, NULL }; + UT_array *tags = NULL; + UT_array *types = NULL; + UT_icd icd = { sizeof(neu_plugin_tag_value_t), NULL, NULL, NULL }; utarray_new(tags, &icd); int value_err = NEU_ERR_SUCCESS; int value_check; @@ -1553,9 +1626,7 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, if (tag != NULL) { value_check = - is_value_in_range(tag->type, cmd->tags[i].value.value.i64, - cmd->tags[i].value.value.d64, - cmd->tags[i].value.type, tag->decimal); + prepare_write_tag(driver, g, tag, &cmd->tags[i].value, &types); } else { nlog_warn("driver write tags not exist, driver: %s, group: %s, " "tag: %s", @@ -1565,19 +1636,6 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, if (tag != NULL && neu_tag_attribute_test(tag, NEU_ATTRIBUTE_WRITE) && value_check == NEU_ERR_SUCCESS) { - if (tag->type == NEU_TYPE_FLOAT || tag->type == NEU_TYPE_DOUBLE) { - if (cmd->tags[i].value.type == NEU_TYPE_INT64) { - cmd->tags[i].value.value.d64 = - (double) cmd->tags[i].value.value.i64; - } - } - - if (tag->decimal != 0) { - cal_decimal(tag->type, cmd->tags[i].value.type, - &cmd->tags[i].value.value, tag->decimal); - } - fix_value(tag, cmd->tags[i].value.type, &cmd->tags[i].value); - neu_plugin_tag_value_t tv = { .tag = neu_tag_dup(tag), .value = cmd->tags[i].value.value, @@ -1604,6 +1662,9 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, neu_free_dvalue(&dvalue); } utarray_free(tags); + if (types != NULL) { + utarray_free(types); + } return value_err; } @@ -1620,6 +1681,9 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, neu_free_dvalue(&dvalue); } utarray_free(tags); + if (types != NULL) { + utarray_free(types); + } return NEU_ERR_TAG_NOT_EXIST; } @@ -1644,6 +1708,7 @@ int neu_adapter_driver_write_tags(neu_adapter_driver_t *driver, wtag.single = false; wtag.req = (void *) req; wtag.tvs = tags; + wtag.types = types; store_write_tag(g, &wtag); @@ -1682,8 +1747,9 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, } } - UT_array *tags = NULL; - UT_icd icd = { sizeof(neu_plugin_tag_value_t), NULL, NULL, NULL }; + UT_array *tags = NULL; + UT_array *types = NULL; + UT_icd icd = { sizeof(neu_plugin_tag_value_t), NULL, NULL, NULL }; utarray_new(tags, &icd); int value_err = NEU_ERR_SUCCESS; int value_check; @@ -1696,10 +1762,8 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, neu_group_find_tag(g->group, cmd->groups[i].tags[k].tag); if (tag != NULL) { - value_check = is_value_in_range( - tag->type, cmd->groups[i].tags[k].value.value.i64, - cmd->groups[i].tags[k].value.value.d64, - cmd->groups[i].tags[k].value.type, tag->decimal); + value_check = prepare_write_tag( + driver, g, tag, &cmd->groups[i].tags[k].value, &types); } else { value_check = NEU_ERR_TAG_NOT_EXIST; } @@ -1707,25 +1771,10 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, if (tag != NULL && neu_tag_attribute_test(tag, NEU_ATTRIBUTE_WRITE) && value_check == NEU_ERR_SUCCESS) { - if (tag->type == NEU_TYPE_FLOAT || - tag->type == NEU_TYPE_DOUBLE) { - if (cmd->groups[i].tags[k].value.type == NEU_TYPE_INT64) { - cmd->groups[i].tags[k].value.value.d64 = - (double) cmd->groups[i].tags[k].value.value.i64; - } - } - - if (tag->decimal != 0) { - cal_decimal(tag->type, cmd->groups[i].tags[k].value.type, - &cmd->groups[i].tags[k].value.value, - tag->decimal); - } - fix_value(tag, cmd->groups[i].tags[k].value.type, - &cmd->groups[i].tags[k].value); - neu_plugin_tag_value_t tv = { .tag = neu_tag_dup(tag), .value = cmd->groups[i].tags[k].value.value, + .type = cmd->groups[i].tags[k].value.type, }; utarray_push_back(tags, &tv); } else { @@ -1745,6 +1794,9 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, neu_tag_free(tv->tag); } utarray_free(tags); + if (types != NULL) { + utarray_free(types); + } return value_err; } @@ -1761,6 +1813,9 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, neu_tag_free(tv->tag); } utarray_free(tags); + if (types != NULL) { + utarray_free(types); + } return NEU_ERR_TAG_NOT_EXIST; } @@ -1791,6 +1846,7 @@ int neu_adapter_driver_write_gtags(neu_adapter_driver_t *driver, wtag.single = false; wtag.req = (void *) req; wtag.tvs = tags; + wtag.types = types; store_write_tag(first_g, &wtag); @@ -1835,9 +1891,9 @@ int neu_adapter_driver_write_tag(neu_adapter_driver_t *driver, return NEU_ERR_PLUGIN_TAG_NOT_ALLOW_WRITE; } - int value_check = is_value_in_range(tag->type, cmd->value.value.i64, - cmd->value.value.d64, - cmd->value.type, tag->decimal); + UT_array *types = NULL; + int value_check = + prepare_write_tag(driver, g, tag, &cmd->value, &types); if (value_check != NEU_ERR_SUCCESS) { nlog_warn("driver write tag value error(%d), driver: %s, group: " @@ -1845,23 +1901,13 @@ int neu_adapter_driver_write_tag(neu_adapter_driver_t *driver, value_check, driver->adapter.name, cmd->group, cmd->tag); driver->adapter.cb_funs.driver.write_response(&driver->adapter, req, value_check); + if (types != NULL) { + utarray_free(types); + } neu_tag_free(tag); return value_check; } - if (tag->type == NEU_TYPE_FLOAT || tag->type == NEU_TYPE_DOUBLE) { - if (cmd->value.type == NEU_TYPE_INT64) { - cmd->value.value.d64 = (double) cmd->value.value.i64; - } - } - - if (tag->decimal != 0) { - cal_decimal(tag->type, cmd->value.type, &cmd->value.value, - tag->decimal); - } - - fix_value(tag, cmd->value.type, &cmd->value); - const neu_reqresp_head_t *head = (const neu_reqresp_head_t *) req; const char * iface = (head && head->sender[0] != '\0') ? head->sender : "unknown"; @@ -1878,6 +1924,7 @@ int neu_adapter_driver_write_tag(neu_adapter_driver_t *driver, wtag.req = (void *) req; wtag.value = cmd->value.value; wtag.tag = neu_tag_dup(tag); + wtag.types = types; store_write_tag(g, &wtag); @@ -2044,6 +2091,9 @@ int neu_adapter_driver_del_group(neu_adapter_driver_t *driver, const char *name) utarray_foreach(find->wt_tags, to_be_write_tag_t *, tag) { + if (tag->types != NULL) { + utarray_free(tag->types); + } if (tag->single) { neu_tag_free(tag->tag); } else { @@ -2199,7 +2249,19 @@ int neu_adapter_driver_validate_tag(neu_adapter_driver_t *driver, return NEU_ERR_TAG_PRECISION_INVALID; } - if (tag->bias != 0) { + if (!isfinite(tag->decimal)) { + return NEU_ERR_TAG_DECIMAL_INVALID; + } + if (!isfinite(tag->bias)) { + return NEU_ERR_TAG_BIAS_INVALID; + } + if (tag->type == NEU_TYPE_AUTO && + !neu_tag_attribute_test(tag, NEU_ATTRIBUTE_READ) && + !neu_tag_attribute_test(tag, NEU_ATTRIBUTE_SUBSCRIBE)) { + return NEU_ERR_TAG_TYPE_NOT_SUPPORT; + } + + if (tag->bias != 0 && tag->type != NEU_TYPE_AUTO) { switch (tag->type) { case NEU_TYPE_INT8: case NEU_TYPE_UINT8: @@ -2252,9 +2314,10 @@ int neu_adapter_driver_add_tag(neu_adapter_driver_t *driver, const char *group, int ret = NEU_ERR_SUCCESS; group_t *find = NULL; - neu_datatag_parse_addr_option(tag, &tag->option); - driver->adapter.module->intf_funs->driver.validate_tag( - driver->adapter.plugin, tag); + ret = neu_adapter_driver_validate_tag(driver, group, tag); + if (ret != NEU_ERR_SUCCESS) { + return ret; + } HASH_FIND_STR(driver->groups, group, find); if (find == NULL) { @@ -2286,6 +2349,9 @@ int neu_adapter_driver_del_tag(neu_adapter_driver_t *driver, const char *group, HASH_FIND_STR(driver->groups, group, find); if (find != NULL) { ret = neu_group_del_tag(find->group, tag); + if (ret == NEU_ERR_SUCCESS) { + neu_driver_cache_del(driver->cache, group, tag); + } } else { ret = NEU_ERR_GROUP_NOT_EXIST; } @@ -2309,32 +2375,17 @@ int neu_adapter_driver_update_tag(neu_adapter_driver_t *driver, int ret = NEU_ERR_SUCCESS; group_t *find = NULL; - if (strlen(tag->name) >= NEU_TAG_NAME_LEN) { - return NEU_ERR_TAG_NAME_TOO_LONG; - } - - if (strlen(tag->address) > NEU_TAG_ADDRESS_LEN) { - return NEU_ERR_TAG_ADDRESS_TOO_LONG; - } - - if (strlen(tag->description) > NEU_TAG_DESCRIPTION_LEN) { - return NEU_ERR_TAG_DESCRIPTION_TOO_LONG; - } - - if (tag->precision > NEU_TAG_FLOAG_PRECISION_MAX) { - return NEU_ERR_TAG_PRECISION_INVALID; - } - - ret = driver->adapter.module->intf_funs->driver.validate_tag( - driver->adapter.plugin, tag); + ret = neu_adapter_driver_validate_tag(driver, group, tag); if (ret != NEU_ERR_SUCCESS) { return ret; } - neu_datatag_parse_addr_option(tag, &tag->option); HASH_FIND_STR(driver->groups, group, find); if (find != NULL) { ret = neu_group_update_tag(find->group, tag); + if (ret == NEU_ERR_SUCCESS) { + neu_driver_cache_del(driver->cache, group, tag->name); + } } else { ret = NEU_ERR_GROUP_NOT_EXIST; } @@ -2403,13 +2454,27 @@ int neu_adapter_driver_query_tag(neu_adapter_driver_t *driver, return ret; } -void neu_adapter_driver_get_value_tag(neu_adapter_driver_t *driver, - const char *group, UT_array **tags) +int neu_adapter_driver_get_value_tag(neu_adapter_driver_t *driver, + const char *group, UT_array **tags) { int ret = neu_adapter_driver_get_tag(driver, group, tags); if (ret == NEU_ERR_SUCCESS) { utarray_foreach(*tags, neu_datatag_t *, tag) { + if (tag->type == NEU_TYPE_AUTO) { + neu_type_e type = NEU_TYPE_AUTO; + uint64_t generation = 0; + group_t * g = find_group(driver, group); + if (g == NULL || neu_group_is_change(g->group, g->timestamp) || + neu_driver_cache_type_get(driver->cache, group, tag->name, + &type, + &generation) != NEU_ERR_SUCCESS) { + utarray_free(*tags); + *tags = NULL; + return NEU_ERR_PLUGIN_TAG_NOT_READY; + } + *tag = effective_tag(tag, type); + } if (tag->decimal != 0) { if (tag->type == NEU_TYPE_UINT8 || tag->type == NEU_TYPE_INT8 || tag->type == NEU_TYPE_INT16 || @@ -2424,8 +2489,9 @@ void neu_adapter_driver_get_value_tag(neu_adapter_driver_t *driver, } } } else { - utarray_new(*tags, neu_tag_get_icd()); + *tags = NULL; } + return ret; } UT_array *neu_adapter_driver_get_read_tag(neu_adapter_driver_t *driver, @@ -2646,8 +2712,7 @@ static void group_change(void *arg, int64_t timestamp, UT_array *tags, value.type = NEU_TYPE_ERROR; value.value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY; - neu_driver_cache_add(group->driver->cache, group->name, tag->name, - value); + neu_driver_cache_add_tag(group->driver->cache, group->name, tag, value); } neu_plugin_group_t grp = { @@ -2689,6 +2754,32 @@ static int write_callback(void *usr_data) utarray_foreach(pending, to_be_write_tag_t *, wtag) { + if (!write_types_valid(group->driver, wtag->types)) { + group->driver->adapter.cb_funs.driver.write_response( + &group->driver->adapter, wtag->req, + NEU_ERR_PLUGIN_TAG_NOT_READY); + if (wtag->single) { + neu_dvalue_t value = { .type = wtag->tag->type, + .value = wtag->value }; + neu_free_dvalue(&value); + neu_tag_free(wtag->tag); + } else { + utarray_foreach(wtag->tvs, neu_plugin_tag_value_t *, tv) + { + neu_dvalue_t value = { .type = tv->type, + .value = tv->value }; + neu_free_dvalue(&value); + neu_tag_free(tv->tag); + } + utarray_free(wtag->tvs); + } + utarray_free(wtag->types); + continue; + } + if (wtag->types != NULL) { + utarray_free(wtag->types); + } + int64_t s_time = 0; int64_t e_time = 0; @@ -2851,6 +2942,8 @@ static void read_report_group(int64_t timestamp, int64_t timeout, tag_value.datatag.bias = tag->bias; + tag_value.value.attrs = value.value.attrs; + if (value.value.type == NEU_TYPE_ERROR) { tag_value.value = value.value; @@ -2858,18 +2951,12 @@ static void read_report_group(int64_t timestamp, int64_t timeout, continue; } - if ((tag->type == NEU_TYPE_FLOAT && isnan(value.value.value.f32)) || - (tag->type == NEU_TYPE_DOUBLE && isnan(value.value.value.d64))) { - tag_value.value.type = NEU_TYPE_ERROR; - tag_value.value.value.i32 = NEU_ERR_PLUGIN_TAG_VALUE_EXPIRED; - utarray_push_back(tag_values, &tag_value); - continue; - } + neu_datatag_t effective = effective_tag(tag, value.resolved_type); - switch (tag->type) { + switch (effective.type) { case NEU_TYPE_UINT16: case NEU_TYPE_INT16: - switch (tag->option.value16.endian) { + switch (effective.option.value16.endian) { case NEU_DATATAG_ENDIAN_B16: value.value.value.u16 = htons(value.value.value.u16); break; @@ -2881,7 +2968,7 @@ static void read_report_group(int64_t timestamp, int64_t timeout, case NEU_TYPE_FLOAT: case NEU_TYPE_UINT32: case NEU_TYPE_INT32: - switch (tag->option.value32.endian) { + switch (effective.option.value32.endian) { case NEU_DATATAG_ENDIAN_LB32: { uint16_t *v1 = (uint16_t *) value.value.value.bytes.bytes; uint16_t *v2 = (uint16_t *) (value.value.value.bytes.bytes + 2); @@ -2909,7 +2996,7 @@ static void read_report_group(int64_t timestamp, int64_t timeout, case NEU_TYPE_DOUBLE: case NEU_TYPE_INT64: case NEU_TYPE_UINT64: - switch (tag->option.value64.endian) { + switch (effective.option.value64.endian) { case NEU_DATATAG_ENDIAN_B64: case NEU_DATATAG_ENDIAN_BB64: value.value.value.u64 = neu_htonll(value.value.value.u64); @@ -2936,6 +3023,16 @@ static void read_report_group(int64_t timestamp, int64_t timeout, break; } + if ((effective.type == NEU_TYPE_FLOAT && + !isfinite(value.value.value.f32)) || + (effective.type == NEU_TYPE_DOUBLE && + !isfinite(value.value.value.d64))) { + tag_value.value.type = NEU_TYPE_ERROR; + tag_value.value.value.i32 = NEU_ERR_PLUGIN_TAG_VALUE_EXPIRED; + utarray_push_back(tag_values, &tag_value); + continue; + } + if (cache_type != NEU_TAG_CACHE_TYPE_NEVER && (timestamp - value.timestamp) > timeout && timeout > 0) { if (value.value.type == NEU_TYPE_PTR) { @@ -2960,7 +3057,7 @@ static void read_report_group(int64_t timestamp, int64_t timeout, double bias = tag->bias; tag_value.value.type = NEU_TYPE_DOUBLE; - switch (tag->type) { + switch (effective.type) { case NEU_TYPE_INT8: tag_value.value.value.d64 = (double) tag_value.value.value.i8 * decimal + bias; @@ -3002,12 +3099,12 @@ static void read_report_group(int64_t timestamp, int64_t timeout, (double) tag_value.value.value.d64 * decimal + bias; break; default: - tag_value.value.type = tag->type; + tag_value.value.type = effective.type; break; } } if (tag->precision == 0 && tag->bias == 0 && - tag->type == NEU_TYPE_DOUBLE) { + effective.type == NEU_TYPE_DOUBLE) { format_tag_value(&tag_value.value); } } @@ -3041,16 +3138,20 @@ static void read_group(int64_t timestamp, int64_t timeout, continue; } + tag_value.value.attrs = value.value.attrs; + if (value.value.type == NEU_TYPE_ERROR) { tag_value.value = value.value; utarray_push_back(tag_values, &tag_value); continue; } - switch (tag->type) { + neu_datatag_t effective = effective_tag(tag, value.resolved_type); + + switch (effective.type) { case NEU_TYPE_UINT16: case NEU_TYPE_INT16: - switch (tag->option.value16.endian) { + switch (effective.option.value16.endian) { case NEU_DATATAG_ENDIAN_B16: value.value.value.u16 = htons(value.value.value.u16); break; @@ -3062,7 +3163,7 @@ static void read_group(int64_t timestamp, int64_t timeout, case NEU_TYPE_FLOAT: case NEU_TYPE_UINT32: case NEU_TYPE_INT32: - switch (tag->option.value32.endian) { + switch (effective.option.value32.endian) { case NEU_DATATAG_ENDIAN_LB32: { uint16_t *v1 = (uint16_t *) value.value.value.bytes.bytes; uint16_t *v2 = (uint16_t *) (value.value.value.bytes.bytes + 2); @@ -3090,7 +3191,7 @@ static void read_group(int64_t timestamp, int64_t timeout, case NEU_TYPE_DOUBLE: case NEU_TYPE_INT64: case NEU_TYPE_UINT64: - switch (tag->option.value64.endian) { + switch (effective.option.value64.endian) { case NEU_DATATAG_ENDIAN_B64: case NEU_DATATAG_ENDIAN_BB64: value.value.value.u64 = neu_htonll(value.value.value.u64); @@ -3117,6 +3218,16 @@ static void read_group(int64_t timestamp, int64_t timeout, break; } + if ((effective.type == NEU_TYPE_FLOAT && + !isfinite(value.value.value.f32)) || + (effective.type == NEU_TYPE_DOUBLE && + !isfinite(value.value.value.d64))) { + tag_value.value.type = NEU_TYPE_ERROR; + tag_value.value.value.i32 = NEU_ERR_PLUGIN_TAG_VALUE_EXPIRED; + utarray_push_back(tag_values, &tag_value); + continue; + } + if (cache_type != NEU_TAG_CACHE_TYPE_NEVER && (timestamp - value.timestamp) > timeout) { if (value.value.type == NEU_TYPE_PTR) { @@ -3139,7 +3250,7 @@ static void read_group(int64_t timestamp, int64_t timeout, tag_value.value.type = NEU_TYPE_DOUBLE; double decimal = tag->decimal != 0 ? tag->decimal : 1; double bias = tag->bias; - switch (tag->type) { + switch (effective.type) { case NEU_TYPE_INT8: tag_value.value.value.d64 = (double) tag_value.value.value.i8 * decimal + bias; @@ -3181,13 +3292,13 @@ static void read_group(int64_t timestamp, int64_t timeout, (double) tag_value.value.value.d64 * decimal + bias; break; default: - tag_value.value.type = tag->type; + tag_value.value.type = effective.type; break; } } if (tag->precision == 0 && tag->bias == 0 && - tag->type == NEU_TYPE_DOUBLE) { + effective.type == NEU_TYPE_DOUBLE) { format_tag_value(&tag_value.value); } } @@ -3236,16 +3347,20 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, continue; } + tag_value.value.attrs = value.value.attrs; + if (value.value.type == NEU_TYPE_ERROR) { tag_value.value = value.value; utarray_push_back(tag_values, &tag_value); continue; } - switch (tag->type) { + neu_datatag_t effective = effective_tag(tag, value.resolved_type); + + switch (effective.type) { case NEU_TYPE_UINT16: case NEU_TYPE_INT16: - switch (tag->option.value16.endian) { + switch (effective.option.value16.endian) { case NEU_DATATAG_ENDIAN_B16: value.value.value.u16 = htons(value.value.value.u16); break; @@ -3257,7 +3372,7 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, case NEU_TYPE_FLOAT: case NEU_TYPE_UINT32: case NEU_TYPE_INT32: - switch (tag->option.value32.endian) { + switch (effective.option.value32.endian) { case NEU_DATATAG_ENDIAN_LB32: { uint16_t *v1 = (uint16_t *) value.value.value.bytes.bytes; uint16_t *v2 = (uint16_t *) (value.value.value.bytes.bytes + 2); @@ -3285,7 +3400,7 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, case NEU_TYPE_DOUBLE: case NEU_TYPE_INT64: case NEU_TYPE_UINT64: - switch (tag->option.value64.endian) { + switch (effective.option.value64.endian) { case NEU_DATATAG_ENDIAN_B64: case NEU_DATATAG_ENDIAN_BB64: value.value.value.u64 = neu_htonll(value.value.value.u64); @@ -3312,6 +3427,16 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, break; } + if ((effective.type == NEU_TYPE_FLOAT && + !isfinite(value.value.value.f32)) || + (effective.type == NEU_TYPE_DOUBLE && + !isfinite(value.value.value.d64))) { + tag_value.value.type = NEU_TYPE_ERROR; + tag_value.value.value.i32 = NEU_ERR_PLUGIN_TAG_VALUE_EXPIRED; + utarray_push_back(tag_values, &tag_value); + continue; + } + if (cache_type != NEU_TAG_CACHE_TYPE_NEVER && (timestamp - value.timestamp) > timeout) { if (value.value.type == NEU_TYPE_PTR) { @@ -3334,7 +3459,7 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, tag_value.value.type = NEU_TYPE_DOUBLE; double decimal = tag->decimal != 0 ? tag->decimal : 1; double bias = tag->bias; - switch (tag->type) { + switch (effective.type) { case NEU_TYPE_INT8: tag_value.value.value.d64 = (double) tag_value.value.value.i8 * decimal + bias; @@ -3376,13 +3501,13 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout, (double) tag_value.value.value.d64 * decimal + bias; break; default: - tag_value.value.type = tag->type; + tag_value.value.type = effective.type; break; } } if (tag->precision == 0 && tag->bias == 0 && - tag->type == NEU_TYPE_DOUBLE) { + effective.type == NEU_TYPE_DOUBLE) { format_tag_value(&tag_value.value); } } diff --git a/src/adapter/driver/driver_internal.h b/src/adapter/driver/driver_internal.h index 1e0684056..e0f9c3643 100644 --- a/src/adapter/driver/driver_internal.h +++ b/src/adapter/driver/driver_internal.h @@ -90,7 +90,7 @@ UT_array *neu_adapter_driver_get_ptag(neu_adapter_driver_t *driver, int neu_adapter_driver_query_tag(neu_adapter_driver_t *driver, const char *group, const char *name, UT_array **tags); -void neu_adapter_driver_get_value_tag(neu_adapter_driver_t *driver, +int neu_adapter_driver_get_value_tag(neu_adapter_driver_t *driver, const char *group, UT_array **tags); UT_array *neu_adapter_driver_get_read_tag(neu_adapter_driver_t *driver, const char * group); diff --git a/src/base/msg_internal.h b/src/base/msg_internal.h index 392d7fb1c..2dde9c9a9 100644 --- a/src/base/msg_internal.h +++ b/src/base/msg_internal.h @@ -174,7 +174,12 @@ extern "C" { XX(NEU_REQ_SERVER_AUTH_USER_INFO, neu_req_server_auth_users_info_t) \ XX(NEU_RESP_SERVER_AUTH_USER_INFO, neu_resp_server_auth_users_info_t) \ XX(NEU_REQ_RENAME_TAG, neu_req_rename_tag_t) \ - XX(NEU_RESP_RENAME_TAG, neu_resp_rename_tag_t) + XX(NEU_RESP_RENAME_TAG, neu_resp_rename_tag_t) \ + XX(NEU_REQ_ADD_NODE_PROPERTIES, neu_req_add_node_properties_t) \ + XX(NEU_REQ_DELETE_NODE_PROPERTIES, neu_req_delete_node_properties_t) \ + XX(NEU_REQ_UPDATE_NODE_PROPERTY, neu_req_update_node_property_t) \ + XX(NEU_REQ_GET_NODE_PROPERTIES, neu_req_get_node_properties_t) \ + XX(NEU_RESP_GET_NODE_PROPERTIES, neu_resp_get_node_properties_t) static inline size_t neu_reqresp_size(neu_reqresp_type_e t) { @@ -235,6 +240,12 @@ static inline neu_msg_t *neu_msg_new(neu_reqresp_type_e t, void *ctx, case NEU_REQ_GET_NODE_SETTING: body_size = neu_reqresp_size(NEU_RESP_GET_NODE_SETTING); break; + case NEU_REQ_GET_NODE_PROPERTIES: + body_size = neu_reqresp_size(NEU_RESP_GET_NODE_PROPERTIES); + if (body_size < data_size) { + body_size = data_size; + } + break; case NEU_REQ_GET_NODES_STATE: body_size = neu_reqresp_size(NEU_RESP_GET_NODES_STATE); break; diff --git a/src/base/property.c b/src/base/property.c new file mode 100644 index 000000000..7535d85c4 --- /dev/null +++ b/src/base/property.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include + +#include "property.h" + +typedef struct { + char * data; + size_t length; + size_t capacity; +} property_json_buffer_t; + +static int append(property_json_buffer_t *buffer, const char *text) +{ + size_t length = strlen(text); + if (length >= SIZE_MAX - buffer->length) { + return -1; + } + size_t needed = buffer->length + length + 1; + if (needed > buffer->capacity) { + size_t capacity = buffer->capacity ? buffer->capacity : 64; + while (capacity < needed) { + if (capacity > SIZE_MAX / 2) { + capacity = needed; + break; + } + capacity *= 2; + } + char *data = realloc(buffer->data, capacity); + if (data == NULL) { + return -1; + } + buffer->data = data; + buffer->capacity = capacity; + } + memcpy(buffer->data + buffer->length, text, length + 1); + buffer->length += length; + return 0; +} + +static int dump_value(property_json_buffer_t *buffer, const json_t *value) +{ + if (json_is_real(value)) { + char text[64]; + int length = + snprintf(text, sizeof(text), "%.17g", json_real_value(value)); + if (length < 0 || (size_t) length >= sizeof(text) - 2) { + return -1; + } + const char *point = localeconv()->decimal_point; + char * decimal = *point ? strstr(text, point) : NULL; + if (decimal != NULL && strcmp(point, ".") != 0) { + memmove(decimal + 1, decimal + strlen(point), + strlen(decimal + strlen(point)) + 1); + *decimal = '.'; + } + if (strpbrk(text, ".eE") == NULL) { + strcat(text, ".0"); + } + return append(buffer, text); + } + if (json_is_array(value)) { + if (append(buffer, "[") != 0) { + return -1; + } + for (size_t i = 0; i < json_array_size(value); ++i) { + if ((i != 0 && append(buffer, ",") != 0) || + dump_value(buffer, json_array_get(value, i)) != 0) { + return -1; + } + } + return append(buffer, "]"); + } + if (json_is_object(value)) { + if (append(buffer, "{") != 0) { + return -1; + } + const char *key; + json_t * item; + bool first = true; + json_object_foreach((json_t *) value, key, item) + { + json_t *name = json_string(key); + if (name == NULL) { + return -1; + } + int ret = (!first && append(buffer, ",") != 0) || + dump_value(buffer, name) != 0 || append(buffer, ":") != 0 || + dump_value(buffer, item) != 0; + json_decref(name); + if (ret != 0) { + return -1; + } + first = false; + (void) first; + } + return append(buffer, "}"); + } + char *text = json_dumps(value, JSON_COMPACT | JSON_ENCODE_ANY); + if (text == NULL) { + return -1; + } + int ret = append(buffer, text); + free(text); + return ret; +} + +char *neu_node_property_json_dump(const json_t *value) +{ + property_json_buffer_t buffer = { 0 }; + if (dump_value(&buffer, value) != 0) { + free(buffer.data); + return NULL; + } + return buffer.data; +} diff --git a/src/core/manager.c b/src/core/manager.c index b7bd23da7..c1b28e939 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -277,6 +277,72 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) neu_msg_free(msg); break; } + case NEU_REQ_ADD_NODE_PROPERTIES: { + neu_req_add_node_properties_t *cmd = + (neu_req_add_node_properties_t *) &header[1]; + neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST }; + + if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) { + e.error = neu_persister_add_node_properties( + cmd->node, cmd->properties, cmd->n_property); + } + neu_req_add_node_properties_fini(cmd); + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &e); + break; + } + case NEU_REQ_DELETE_NODE_PROPERTIES: { + neu_req_delete_node_properties_t *cmd = + (neu_req_delete_node_properties_t *) &header[1]; + neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST }; + + if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) { + e.error = neu_persister_delete_node_properties( + cmd->node, (const char *const *) cmd->keys, cmd->n_key); + } + neu_req_delete_node_properties_fini(cmd); + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &e); + break; + } + case NEU_REQ_UPDATE_NODE_PROPERTY: { + neu_req_update_node_property_t *cmd = + (neu_req_update_node_property_t *) &header[1]; + neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST }; + + if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) { + e.error = + neu_persister_update_node_property(cmd->node, &cmd->property); + } + neu_req_update_node_property_fini(cmd); + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &e); + break; + } + case NEU_REQ_GET_NODE_PROPERTIES: { + neu_req_get_node_properties_t *cmd = + (neu_req_get_node_properties_t *) &header[1]; + neu_resp_get_node_properties_t resp = { 0 }; + neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST }; + + if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) { + e.error = neu_persister_load_node_properties( + cmd->node, &resp.properties, &resp.n_property); + } + neu_msg_exchange(header); + if (e.error == NEU_ERR_SUCCESS) { + header->type = NEU_RESP_GET_NODE_PROPERTIES; + reply(manager, header, &resp); + } else { + neu_resp_get_node_properties_fini(&resp); + header->type = NEU_RESP_ERROR; + reply(manager, header, &e); + } + break; + } case NEU_REQ_UPDATE_NODE_TAG: { neu_req_update_node_tag_t *cmd = (neu_req_update_node_tag_t *) &header[1]; @@ -1117,7 +1183,8 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) case NEU_REQ_GET_SUB_DRIVER_TAGS: { neu_req_get_sub_driver_tags_t *cmd = (neu_req_get_sub_driver_tags_t *) &header[1]; - neu_resp_get_sub_driver_tags_t resp = { 0 }; + neu_resp_get_sub_driver_tags_t resp = { 0 }; + int value_error = NEU_ERR_SUCCESS; UT_array *groups = neu_manager_get_sub_group(manager, cmd->app); utarray_new(resp.infos, neu_resp_get_sub_driver_tags_info_icd()); @@ -1130,12 +1197,28 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) snprintf(in.driver, sizeof(in.driver), "%s", info->driver); snprintf(in.group, sizeof(in.group), "%s", info->group); - neu_adapter_driver_get_value_tag((neu_adapter_driver_t *) driver, - info->group, &in.tags); + value_error = neu_adapter_driver_get_value_tag( + (neu_adapter_driver_t *) driver, info->group, &in.tags); + if (value_error != NEU_ERR_SUCCESS) { + break; + } utarray_push_back(resp.infos, &in); } utarray_free(groups); + if (value_error != NEU_ERR_SUCCESS) { + utarray_foreach(resp.infos, neu_resp_get_sub_driver_tags_info_t *, + in) + { + utarray_free(in->tags); + } + utarray_free(resp.infos); + neu_resp_error_t error = { .error = value_error }; + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &error); + break; + } snprintf(header->receiver, sizeof(header->receiver), "%s", header->sender); @@ -1160,7 +1243,8 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) case NEU_REQ_GET_DATALAYERS_TAGS: { neu_req_get_sub_driver_tags_t *cmd = (neu_req_get_sub_driver_tags_t *) &header[1]; - neu_resp_get_sub_driver_tags_t resp = { 0 }; + neu_resp_get_sub_driver_tags_t resp = { 0 }; + int value_error = NEU_ERR_SUCCESS; UT_array *groups = neu_manager_get_sub_group(manager, cmd->app); bool found = false; @@ -1181,12 +1265,28 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) snprintf(in.driver, sizeof(in.driver), "%s", info->driver); snprintf(in.group, sizeof(in.group), "%s", info->group); - neu_adapter_driver_get_value_tag((neu_adapter_driver_t *) driver, - info->group, &in.tags); + value_error = neu_adapter_driver_get_value_tag( + (neu_adapter_driver_t *) driver, info->group, &in.tags); + if (value_error != NEU_ERR_SUCCESS) { + break; + } utarray_push_back(resp.infos, &in); } utarray_free(groups); + if (value_error != NEU_ERR_SUCCESS) { + utarray_foreach(resp.infos, neu_resp_get_sub_driver_tags_info_t *, + in) + { + utarray_free(in->tags); + } + utarray_free(resp.infos); + neu_resp_error_t error = { .error = value_error }; + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &error); + break; + } if (!found) { utarray_free(resp.infos); @@ -1207,7 +1307,8 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) case NEU_REQ_GET_DATALAYERS_TAG: { neu_req_get_sub_driver_tags_t *cmd = (neu_req_get_sub_driver_tags_t *) &header[1]; - neu_resp_get_sub_driver_tags_t resp = { 0 }; + neu_resp_get_sub_driver_tags_t resp = { 0 }; + int value_error = NEU_ERR_SUCCESS; UT_array *groups = neu_manager_get_sub_group(manager, cmd->app); bool found = false; @@ -1220,8 +1321,11 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) neu_node_manager_find(manager->node_manager, info->driver); assert(driver != NULL); - neu_adapter_driver_get_value_tag((neu_adapter_driver_t *) driver, - info->group, &in.tags); + value_error = neu_adapter_driver_get_value_tag( + (neu_adapter_driver_t *) driver, info->group, &in.tags); + if (value_error != NEU_ERR_SUCCESS) { + break; + } if (in.tags) { UT_array *filtered_tags = NULL; @@ -1249,6 +1353,19 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) } utarray_free(groups); + if (value_error != NEU_ERR_SUCCESS) { + utarray_foreach(resp.infos, neu_resp_get_sub_driver_tags_info_t *, + in) + { + utarray_free(in->tags); + } + utarray_free(resp.infos); + neu_resp_error_t error = { .error = value_error }; + header->type = NEU_RESP_ERROR; + neu_msg_exchange(header); + reply(manager, header, &error); + break; + } if (!found) { utarray_free(resp.infos); @@ -1645,6 +1762,12 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data) break; } + case NEU_RESP_GET_NODE_PROPERTIES: { + neu_resp_get_node_properties_t resp = + *(neu_resp_get_node_properties_t *) &header[1]; + reply(manager, header, &resp); + break; + } case NEU_RESP_ADD_TAG: case NEU_RESP_ADD_GTAG: case NEU_RESP_IMPORT_TAGS: @@ -1923,6 +2046,10 @@ inline static void reply(neu_manager_t *manager, neu_reqresp_head_t *header, } else { nlog_warn("reply %s to %s, error: %d", neu_reqresp_type_string(t), receiver, ret); + if (t == NEU_RESP_GET_NODE_PROPERTIES) { + neu_resp_get_node_properties_fini( + (neu_resp_get_node_properties_t *) neu_msg_get_body(msg)); + } neu_msg_free(msg); } } diff --git a/src/parser/neu_json_node.c b/src/parser/neu_json_node.c index 38808f203..b22e62a69 100644 --- a/src/parser/neu_json_node.c +++ b/src/parser/neu_json_node.c @@ -30,6 +30,7 @@ #include "json/json.h" #include "neu_json_node.h" +#include "utils/set.h" int neu_json_encode_add_node_req(void *json_object, void *param) { @@ -748,4 +749,212 @@ void neu_json_decode_update_node_tag_req_free( free(req->tags); free(req); } +} + +static char *property_string(json_t *value, size_t max_length) +{ + if (!json_is_string(value)) { + return NULL; + } + size_t length = json_string_length(value); + const char *str = json_string_value(value); + if (length == 0 || length >= max_length || strlen(str) != length) { + return NULL; + } + return strdup(str); +} + +static int decode_property(json_t *object, neu_node_property_t *property) +{ + property->key = property_string(json_object_get(object, "key"), SIZE_MAX); + json_t *value = json_object_get(object, "value"); + if (property->key == NULL || !neu_node_property_value_valid(value)) { + return -1; + } + property->value_json = neu_node_property_json_dump(value); + return property->value_json == NULL ? -1 : 0; +} + +int neu_json_decode_add_node_properties_req( + char *buf, neu_json_add_node_properties_req_t **result) +{ + *result = NULL; + json_t *object = + json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL); + neu_json_add_node_properties_req_t *req = calloc(1, sizeof(*req)); + neu_strset_t seen = NULL; + int ret = -1; + if (req == NULL || !json_is_object(object)) { + goto end; + } + req->node = + property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN); + json_t *array = json_object_get(object, "properties"); + if (req->node == NULL || !json_is_array(array) || + json_array_size(array) == 0) { + goto end; + } + size_t count = json_array_size(array); + req->properties = calloc(count, sizeof(*req->properties)); + if (req->properties == NULL) { + goto end; + } + req->n_property = count; + for (size_t i = 0; i < count; ++i) { + if (decode_property(json_array_get(array, i), &req->properties[i]) != + 0 || + neu_strset_add(&seen, req->properties[i].key) != 1) { + goto end; + } + } + *result = req; + ret = 0; +end: + neu_strset_free(&seen); + json_decref(object); + if (ret != 0) { + neu_json_decode_add_node_properties_req_free(req); + } + return ret; +} + +void neu_json_decode_add_node_properties_req_free( + neu_json_add_node_properties_req_t *req) +{ + if (req != NULL) { + free(req->node); + neu_node_properties_free(req->properties, req->n_property); + free(req); + } +} + +int neu_json_decode_delete_node_properties_req( + char *buf, neu_json_delete_node_properties_req_t **result) +{ + *result = NULL; + json_t *object = + json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL); + neu_json_delete_node_properties_req_t *req = calloc(1, sizeof(*req)); + neu_strset_t seen = NULL; + int ret = -1; + if (req == NULL || !json_is_object(object)) { + goto end; + } + req->node = + property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN); + json_t *array = json_object_get(object, "keys"); + if (req->node == NULL || !json_is_array(array) || + json_array_size(array) == 0) { + goto end; + } + size_t count = json_array_size(array); + req->keys = calloc(count, sizeof(*req->keys)); + if (req->keys == NULL) { + goto end; + } + req->n_key = count; + for (size_t i = 0; i < count; ++i) { + req->keys[i] = property_string(json_array_get(array, i), SIZE_MAX); + if (req->keys[i] == NULL || neu_strset_add(&seen, req->keys[i]) != 1) { + goto end; + } + } + *result = req; + ret = 0; +end: + neu_strset_free(&seen); + json_decref(object); + if (ret != 0) { + neu_json_decode_delete_node_properties_req_free(req); + } + return ret; +} + +void neu_json_decode_delete_node_properties_req_free( + neu_json_delete_node_properties_req_t *req) +{ + if (req != NULL) { + free(req->node); + for (size_t i = 0; i < req->n_key; ++i) { + free(req->keys[i]); + } + free(req->keys); + free(req); + } +} + +int neu_json_decode_update_node_property_req( + char *buf, neu_json_update_node_property_req_t **result) +{ + *result = NULL; + json_t *object = + json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL); + neu_json_update_node_property_req_t *req = calloc(1, sizeof(*req)); + if (req == NULL || !json_is_object(object)) { + goto error; + } + req->node = + property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN); + if (req->node == NULL || decode_property(object, &req->property) != 0) { + goto error; + } + json_decref(object); + *result = req; + return 0; +error: + json_decref(object); + neu_json_decode_update_node_property_req_free(req); + return -1; +} + +void neu_json_decode_update_node_property_req_free( + neu_json_update_node_property_req_t *req) +{ + if (req != NULL) { + free(req->node); + neu_node_property_fini(&req->property); + free(req); + } +} + +static int +encode_node_properties_resp(json_t * json_object, + neu_json_get_node_properties_resp_t *resp) +{ + json_t *array = json_array(); + if (array == NULL) { + return -1; + } + for (size_t i = 0; i < resp->n_property; ++i) { + neu_node_property_t *property = &resp->properties[i]; + json_t * value = json_loads(property->value_json, + JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL); + if (!neu_node_property_value_valid(value)) { + json_decref(value); + json_decref(array); + return -1; + } + json_t *item = + json_pack("{s:s,s:o}", "key", property->key, "value", value); + if (item == NULL || json_array_append_new(array, item) != 0) { + json_decref(array); + return -1; + } + } + return json_object_set_new(json_object, "properties", array); +} + +int neu_json_encode_get_node_properties_resp( + neu_json_get_node_properties_resp_t *resp, char **result) +{ + *result = NULL; + json_t *object = json_object(); + if (object == NULL) { + return -1; + } + if (encode_node_properties_resp(object, resp) == 0) { + *result = neu_node_property_json_dump(object); + } + json_decref(object); + return *result == NULL ? -1 : 0; } \ No newline at end of file diff --git a/src/parser/neu_json_node.h b/src/parser/neu_json_node.h index 7098c7429..9bae17d35 100644 --- a/src/parser/neu_json_node.h +++ b/src/parser/neu_json_node.h @@ -26,6 +26,7 @@ #define _NEU_JSON_API_NEU_JSON_NODE_H_ #include "define.h" +#include "property.h" #include "json/json.h" #ifdef __cplusplus @@ -149,6 +150,43 @@ typedef struct { int neu_json_encode_get_node_setting_resp(void *json_object, void *param); +typedef struct { + char * node; + size_t n_property; + neu_node_property_t *properties; +} neu_json_add_node_properties_req_t; + +typedef struct { + char * node; + size_t n_key; + char **keys; +} neu_json_delete_node_properties_req_t; + +typedef struct { + char * node; + neu_node_property_t property; +} neu_json_update_node_property_req_t; + +typedef struct { + size_t n_property; + neu_node_property_t *properties; +} neu_json_get_node_properties_resp_t; + +int neu_json_decode_add_node_properties_req( + char *buf, neu_json_add_node_properties_req_t **result); +void neu_json_decode_add_node_properties_req_free( + neu_json_add_node_properties_req_t *req); +int neu_json_decode_delete_node_properties_req( + char *buf, neu_json_delete_node_properties_req_t **result); +void neu_json_decode_delete_node_properties_req_free( + neu_json_delete_node_properties_req_t *req); +int neu_json_decode_update_node_property_req( + char *buf, neu_json_update_node_property_req_t **result); +void neu_json_decode_update_node_property_req_free( + neu_json_update_node_property_req_t *req); +int neu_json_encode_get_node_properties_resp( + neu_json_get_node_properties_resp_t *resp, char **result); + #ifdef __cplusplus } #endif diff --git a/src/persist/persist.c b/src/persist/persist.c index 81dfe5b14..99d0c7a04 100644 --- a/src/persist/persist.c +++ b/src/persist/persist.c @@ -360,6 +360,34 @@ int neu_persister_update_node_state(const char *node_name, int state) return g_impl->vtbl->update_node_state(g_impl, node_name, state); } +int neu_persister_add_node_properties(const char * node, + const neu_node_property_t *properties, + size_t n_property) +{ + return g_impl->vtbl->add_node_properties(g_impl, node, properties, + n_property); +} + +int neu_persister_delete_node_properties(const char * node, + const char *const *keys, size_t n_key) +{ + return g_impl->vtbl->delete_node_properties(g_impl, node, keys, n_key); +} + +int neu_persister_update_node_property(const char * node, + const neu_node_property_t *property) +{ + return g_impl->vtbl->update_node_property(g_impl, node, property); +} + +int neu_persister_load_node_properties(const char * node, + neu_node_property_t **properties, + size_t * n_property) +{ + return g_impl->vtbl->load_node_properties(g_impl, node, properties, + n_property); +} + int neu_persister_store_tag(const char *driver_name, const char *group_name, const neu_datatag_t *tag) { diff --git a/src/persist/persist_impl.h b/src/persist/persist_impl.h index 63d6eb385..ca79fbd5d 100644 --- a/src/persist/persist_impl.h +++ b/src/persist/persist_impl.h @@ -85,6 +85,17 @@ struct neu_persister_vtbl_s { int (*update_node_state)(neu_persister_t *self, const char *node_name, int state); + int (*add_node_properties)(neu_persister_t *self, const char *node, + const neu_node_property_t *properties, + size_t n_property); + int (*delete_node_properties)(neu_persister_t *self, const char *node, + const char *const *keys, size_t n_key); + int (*update_node_property)(neu_persister_t *self, const char *node, + const neu_node_property_t *property); + int (*load_node_properties)(neu_persister_t *self, const char *node, + neu_node_property_t **properties, + size_t * n_property); + /** * Persist node setting. * @param adapter_name name of the adapter who owns the diff --git a/src/persist/sqlite.c b/src/persist/sqlite.c index dacac4738..59229204e 100644 --- a/src/persist/sqlite.c +++ b/src/persist/sqlite.c @@ -29,6 +29,7 @@ #include "errcodes.h" #include "utils/log.h" +#include "utils/set.h" #include "sqlite.h" @@ -376,9 +377,18 @@ static int apply_schemas(sqlite3 *db, const char *dir) static inline int open_db(const char *schema_dir, sqlite3 **db_p) { sqlite3 *db = NULL; - int rv = sqlite3_open(DB_FILE, &db); + int rv = sqlite3_open_v2(DB_FILE, &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, + NULL); if (SQLITE_OK != rv) { nlog_fatal("db `%s` fail: %s", DB_FILE, sqlite3_errstr(rv)); + sqlite3_close(db); + return -1; + } + if (NULL == sqlite3_db_mutex(db)) { + nlog_fatal("db mutex unavailable"); + sqlite3_close(db); return -1; } sqlite3_busy_timeout(db, 100 * 1000); @@ -416,6 +426,10 @@ static struct neu_persister_vtbl_s g_sqlite_persister_vtbl = { .delete_node = neu_sqlite_persister_delete_node, .update_node = neu_sqlite_persister_update_node, .update_node_state = neu_sqlite_persister_update_node_state, + .add_node_properties = neu_sqlite_persister_add_node_properties, + .delete_node_properties = neu_sqlite_persister_delete_node_properties, + .update_node_property = neu_sqlite_persister_update_node_property, + .load_node_properties = neu_sqlite_persister_load_node_properties, .store_tag = neu_sqlite_persister_store_tag, .store_tags = neu_sqlite_persister_store_tags, .load_tags = neu_sqlite_persister_load_tags, @@ -612,6 +626,276 @@ int neu_sqlite_persister_update_node_state(neu_persister_t *self, node_name); } +static int begin_transaction(sqlite3 *db) +{ + if (!sqlite3_get_autocommit(db) || + SQLITE_OK != sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL)) { + nlog_error("begin transaction fail: %s", sqlite3_errmsg(db)); + return NEU_ERR_EINTERNAL; + } + return 0; +} + +static void rollback_transaction(sqlite3 *db) +{ + if (!sqlite3_get_autocommit(db) && + SQLITE_OK != sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL)) { + nlog_error("rollback transaction fail: %s", sqlite3_errmsg(db)); + } +} + +static int node_property_json(const char *text, char **normalized) +{ + *normalized = NULL; + if (NULL == text) { + return NEU_ERR_PARAM_IS_WRONG; + } + + json_error_t error; + json_t *value = json_loads(text, JSON_DECODE_ANY | JSON_ALLOW_NUL, &error); + if (NULL == value) { + return json_error_code(&error) == json_error_out_of_memory + ? NEU_ERR_EINTERNAL + : NEU_ERR_PARAM_IS_WRONG; + } + if (!neu_node_property_value_valid(value)) { + json_decref(value); + return NEU_ERR_PARAM_IS_WRONG; + } + + *normalized = neu_node_property_json_dump(value); + json_decref(value); + return *normalized ? 0 : NEU_ERR_EINTERNAL; +} + +enum node_property_operation { + NODE_PROPERTY_ADD, + NODE_PROPERTY_DELETE, + NODE_PROPERTY_UPDATE, +}; + +static int write_node_properties(neu_persister_t *self, const char *node, + const neu_node_property_t *properties, + const char *const *keys, size_t n, + enum node_property_operation operation) +{ + if (NULL == self || NULL == node || '\0' == *node || 0 == n || + (NODE_PROPERTY_DELETE == operation ? NULL == keys + : NULL == properties)) { + return NEU_ERR_PARAM_IS_WRONG; + } + + sqlite3 * db = ((neu_sqlite_persister_t *) self)->db; + sqlite3_mutex *mutex = db ? sqlite3_db_mutex(db) : NULL; + if (NULL == mutex) { + return NEU_ERR_EINTERNAL; + } + sqlite3_mutex_enter(mutex); + + int rv = NEU_ERR_EINTERNAL; + bool begun = false; + sqlite3_stmt *stmt = NULL; + char * value_json = NULL; + neu_strset_t seen = NULL; + const char * query = NODE_PROPERTY_ADD == operation + ? "INSERT INTO node_properties(node_name, key, value_json) VALUES(?1, " + "?2, ?3)" + : NODE_PROPERTY_DELETE == operation + ? "DELETE FROM node_properties WHERE node_name=?1 AND key=?2" + : "UPDATE node_properties SET value_json=?3 WHERE node_name=?1 AND " + "key=?2"; + + if (0 != begin_transaction(db)) { + goto end; + } + begun = true; + if (SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, NULL) || + SQLITE_OK != sqlite3_bind_text(stmt, 1, node, -1, SQLITE_TRANSIENT)) { + goto end; + } + + for (size_t i = 0; i < n; ++i) { + const char *key = + NODE_PROPERTY_DELETE == operation ? keys[i] : properties[i].key; + if (NULL == key || '\0' == *key) { + rv = NEU_ERR_PARAM_IS_WRONG; + goto end; + } + if (NODE_PROPERTY_DELETE == operation) { + int added = neu_strset_add(&seen, key); + if (1 != added) { + rv = added == 0 ? NEU_ERR_PARAM_IS_WRONG : NEU_ERR_EINTERNAL; + goto end; + } + } else { + rv = node_property_json(properties[i].value_json, &value_json); + if (0 != rv) { + goto end; + } + } + + rv = NEU_ERR_EINTERNAL; + if (SQLITE_OK != sqlite3_reset(stmt) || + SQLITE_OK != + sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT) || + (NODE_PROPERTY_DELETE != operation && + SQLITE_OK != + sqlite3_bind_text(stmt, 3, value_json, -1, + SQLITE_TRANSIENT))) { + goto end; + } + free(value_json); + value_json = NULL; + + if (SQLITE_DONE != sqlite3_step(stmt)) { + int code = sqlite3_extended_errcode(db); + if (NODE_PROPERTY_ADD == operation && + (SQLITE_CONSTRAINT_PRIMARYKEY == code || + SQLITE_CONSTRAINT_UNIQUE == code)) { + rv = NEU_ERR_NODE_PROPERTY_EXIST; + } + goto end; + } + if (NODE_PROPERTY_UPDATE == operation && 0 == sqlite3_changes(db)) { + rv = NEU_ERR_NODE_PROPERTY_NOT_EXIST; + goto end; + } + } + + rv = sqlite3_finalize(stmt); + stmt = NULL; + if (SQLITE_OK != rv || + SQLITE_OK != sqlite3_exec(db, "COMMIT", NULL, NULL, NULL)) { + rv = NEU_ERR_EINTERNAL; + goto end; + } + begun = false; + rv = 0; + +end: + neu_strset_free(&seen); + free(value_json); + sqlite3_finalize(stmt); + if (begun) { + rollback_transaction(db); + } + sqlite3_mutex_leave(mutex); + return rv; +} + +int neu_sqlite_persister_add_node_properties( + neu_persister_t *self, const char *node, + const neu_node_property_t *properties, size_t n_property) +{ + return write_node_properties(self, node, properties, NULL, n_property, + NODE_PROPERTY_ADD); +} + +int neu_sqlite_persister_delete_node_properties(neu_persister_t * self, + const char * node, + const char *const *keys, + size_t n_key) +{ + return write_node_properties(self, node, NULL, keys, n_key, + NODE_PROPERTY_DELETE); +} + +int neu_sqlite_persister_update_node_property( + neu_persister_t *self, const char *node, + const neu_node_property_t *property) +{ + return write_node_properties(self, node, property, NULL, 1, + NODE_PROPERTY_UPDATE); +} + +int neu_sqlite_persister_load_node_properties(neu_persister_t * self, + const char * node, + neu_node_property_t **properties, + size_t * n_property) +{ + if (properties) { + *properties = NULL; + } + if (n_property) { + *n_property = 0; + } + if (NULL == self || NULL == node || '\0' == *node || NULL == properties || + NULL == n_property) { + return NEU_ERR_PARAM_IS_WRONG; + } + + sqlite3 * db = ((neu_sqlite_persister_t *) self)->db; + sqlite3_mutex *mutex = db ? sqlite3_db_mutex(db) : NULL; + if (NULL == mutex) { + return NEU_ERR_EINTERNAL; + } + sqlite3_mutex_enter(mutex); + + int rv = NEU_ERR_EINTERNAL; + sqlite3_stmt * stmt = NULL; + neu_node_property_t *items = NULL; + size_t n = 0; + size_t capacity = 0; + const char * query = "SELECT key, value_json FROM node_properties " + "WHERE node_name=?1 ORDER BY key"; + if (!sqlite3_get_autocommit(db) || + SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, NULL) || + SQLITE_OK != sqlite3_bind_text(stmt, 1, node, -1, SQLITE_TRANSIENT)) { + goto end; + } + + int step; + while (SQLITE_ROW == (step = sqlite3_step(stmt))) { + const char *key = (const char *) sqlite3_column_text(stmt, 0); + const char *text = (const char *) sqlite3_column_text(stmt, 1); + if (NULL == key || '\0' == *key || NULL == text || + strlen(key) != (size_t) sqlite3_column_bytes(stmt, 0) || + strlen(text) != (size_t) sqlite3_column_bytes(stmt, 1)) { + goto end; + } + if (n == capacity) { + if (capacity > SIZE_MAX / 2 / sizeof(*items)) { + goto end; + } + size_t next = capacity ? capacity * 2 : 8; + neu_node_property_t *grown = realloc(items, next * sizeof(*items)); + if (NULL == grown) { + goto end; + } + items = grown; + capacity = next; + } + neu_node_property_t *item = &items[n++]; + *item = (neu_node_property_t) { 0 }; + item->key = strdup(key); + if (NULL == item->key || + 0 != node_property_json(text, &item->value_json)) { + goto end; + } + } + if (SQLITE_DONE != step) { + goto end; + } + rv = sqlite3_finalize(stmt); + stmt = NULL; + if (SQLITE_OK != rv) { + rv = NEU_ERR_EINTERNAL; + goto end; + } + + *properties = items; + *n_property = n; + items = NULL; + n = 0; + rv = 0; + +end: + sqlite3_finalize(stmt); + neu_node_properties_free(items, n); + sqlite3_mutex_leave(mutex); + return rv; +} + int neu_sqlite_persister_store_tag(neu_persister_t * self, const char * driver_name, const char * group_name, @@ -646,7 +930,10 @@ static int put_tags(sqlite3 *db, const char *query, sqlite3_stmt *stmt, neu_tag_format_str(tag, format_buf, sizeof(format_buf)); } - sqlite3_reset(stmt); + if (SQLITE_OK != sqlite3_reset(stmt)) { + nlog_error("reset `%s` fail: %s", query, sqlite3_errmsg(db)); + return -1; + } if (SQLITE_OK != sqlite3_bind_text(stmt, 3, tag->name, -1, NULL)) { nlog_error("bind `%s` with name=`%s` fail: %s", query, tag->name, @@ -703,7 +990,8 @@ static int put_tags(sqlite3 *db, const char *query, sqlite3_stmt *stmt, return -1; } - if (SQLITE_OK != sqlite3_bind_text(stmt, 12, format_buf, -1, NULL)) { + if (SQLITE_OK != + sqlite3_bind_text(stmt, 12, format_buf, -1, SQLITE_TRANSIENT)) { nlog_error("bind `%s` with format=`%s` fail: %s", query, format_buf, sqlite3_errmsg(db)); return -1; @@ -738,10 +1026,18 @@ int neu_sqlite_persister_store_tags(neu_persister_t * self, " precision, type, decimal, bias, description, value, format, unit" ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)"; - if (SQLITE_OK != sqlite3_exec(persister->db, "BEGIN", NULL, NULL, NULL)) { - nlog_error("begin transaction fail: %s", sqlite3_errmsg(persister->db)); + sqlite3_mutex *mutex = sqlite3_db_mutex(persister->db); + if (NULL == mutex) { return NEU_ERR_EINTERNAL; } + sqlite3_mutex_enter(mutex); + bool begun = false; + int rv = NEU_ERR_EINTERNAL; + + if (0 != begin_transaction(persister->db)) { + goto end; + } + begun = true; if (SQLITE_OK != sqlite3_prepare_v2(persister->db, query, -1, &stmt, NULL)) { @@ -766,20 +1062,30 @@ int neu_sqlite_persister_store_tags(neu_persister_t * self, goto error; } + if (SQLITE_OK != sqlite3_finalize(stmt)) { + stmt = NULL; + goto error; + } + stmt = NULL; if (SQLITE_OK != sqlite3_exec(persister->db, "COMMIT", NULL, NULL, NULL)) { nlog_error("commit transaction fail: %s", sqlite3_errmsg(persister->db)); goto error; } - sqlite3_finalize(stmt); - return 0; + begun = false; + rv = 0; + goto end; error: nlog_warn("rollback transaction"); - sqlite3_exec(persister->db, "ROLLBACK", NULL, NULL, NULL); +end: sqlite3_finalize(stmt); - return NEU_ERR_EINTERNAL; + if (begun) { + rollback_transaction(persister->db); + } + sqlite3_mutex_leave(mutex); + return rv; } static int collect_tag_info(sqlite3_stmt *stmt, UT_array **tags) diff --git a/src/persist/sqlite.h b/src/persist/sqlite.h index c9d8ba354..a5fa353e6 100644 --- a/src/persist/sqlite.h +++ b/src/persist/sqlite.h @@ -50,6 +50,21 @@ int neu_sqlite_persister_update_node_tags(neu_persister_t *self, const char * tags); int neu_sqlite_persister_update_node_state(neu_persister_t *self, const char *node_name, int state); +int neu_sqlite_persister_add_node_properties( + neu_persister_t *self, const char *node, + const neu_node_property_t *properties, size_t n_property); +int neu_sqlite_persister_delete_node_properties(neu_persister_t * self, + const char * node, + const char *const *keys, + size_t n_key); +int neu_sqlite_persister_update_node_property( + neu_persister_t *self, const char *node, + const neu_node_property_t *property); +int neu_sqlite_persister_load_node_properties(neu_persister_t * self, + const char * node, + neu_node_property_t **properties, + size_t * n_property); + int neu_sqlite_persister_store_tag(neu_persister_t * self, const char * driver_name, const char * group_name, diff --git a/src/utils/http.c b/src/utils/http.c index d78601701..45c629188 100644 --- a/src/utils/http.c +++ b/src/utils/http.c @@ -364,6 +364,7 @@ int neu_http_response(nng_aio *aio, neu_err_code_e code, char *content) case NEU_ERR_FILE_READ_FAILURE: case NEU_ERR_LIBRARY_NOT_FOUND: case NEU_ERR_NODE_NOT_EXIST: + case NEU_ERR_NODE_PROPERTY_NOT_EXIST: case NEU_ERR_TAG_NOT_EXIST: case NEU_ERR_LICENSE_NOT_FOUND: case NEU_ERR_LICENSE_TOKEN_NOT_FOUND: @@ -377,6 +378,7 @@ int neu_http_response(nng_aio *aio, neu_err_code_e code, char *content) status = NNG_HTTP_STATUS_NOT_FOUND; break; case NEU_ERR_NODE_EXIST: + case NEU_ERR_NODE_PROPERTY_EXIST: case NEU_ERR_NODE_NOT_READY: case NEU_ERR_NODE_IS_RUNNING: case NEU_ERR_NODE_NOT_RUNNING: diff --git a/tests/ft/auto_type_http.py b/tests/ft/auto_type_http.py new file mode 100644 index 000000000..7e36c3840 --- /dev/null +++ b/tests/ft/auto_type_http.py @@ -0,0 +1,377 @@ +"""Isolated real-HTTP AUTO tests; run directly, never through the FT pytest fixtures. + + python3 tests/ft/auto_type_http.py --build build + +Build neuron, plugin-auto-type and plugin-modbus-tcp first. This runner refuses to +share a running Neuron instance and creates its own temporary config, plugins, +logs and database. +The runtime is retained on failure (or with --keep-runtime). + +Mock params replace the previous settings: hold (default true), concrete numeric +NEU_TYPE type (default INT16), value (default 123), error (default 0), update_mode +(update/meta/trace/im/im_f_m), pointer (STRING/BYTES wrapper), optional quality and +timestamp, record_path (append JSONL writes: tag, actual type, union value, path). +All readable tags receive the selected sample; group_sync uses the same controls. +""" + +import argparse +import json +import os +from pathlib import Path +import shutil +import signal +import socket +import sqlite3 +import subprocess +import tempfile +import time +import unittest +import urllib.error +import urllib.request + + +REPO = Path(__file__).resolve().parents[2] +BUILD = REPO / "build" +KEEP_RUNTIME = False + + +class AutoTypeHTTP(unittest.TestCase): + @classmethod + def setUpClass(cls): + if subprocess.run(["pgrep", "-x", "neuron"], capture_output=True).returncode == 0: + raise RuntimeError("Stop the existing Neuron instance before running AUTO HTTP tests") + for artifact in (BUILD / "neuron", BUILD / "tests/plugins/libplugin-auto-type.so", + BUILD / "plugins/libplugin-modbus-tcp.so"): + if not artifact.is_file(): + raise RuntimeError(f"Build required artifact first: {artifact}") + cls.root = Path(tempfile.mkdtemp(prefix="neuron-auto-http-")) + print(f"AUTO HTTP runtime: {cls.root}", flush=True) + cls.process = None + cls.token = None + cls.failed = False + cls.addClassCleanup(cls.cleanup) + for directory in ("config", "plugins/schema", "persistence", "logs"): + (cls.root / directory).mkdir(parents=True, exist_ok=True) + for sql in (REPO / "persistence").glob("*.sql"): + shutil.copy2(sql, cls.root / "config" / sql.name) + for name in ("zlog.conf", "dev.conf"): + shutil.copy2(REPO / name, cls.root / "config" / name) + shutil.copy2(BUILD / "tests/plugins/libplugin-auto-type.so", cls.root / "plugins") + shutil.copy2(REPO / "tests/plugins/auto_type/auto_type.json", cls.root / "plugins/schema") + shutil.copy2(BUILD / "plugins/libplugin-modbus-tcp.so", cls.root / "plugins") + shutil.copy2(BUILD / "plugins/schema/modbus-tcp.json", cls.root / "plugins/schema") + plugins = ["libplugin-auto-type.so", "libplugin-modbus-tcp.so"] + (cls.root / "config/default_plugins.json").write_text(json.dumps({"plugins": plugins})) + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + cls.base = f"http://127.0.0.1:{port}/api/v2" + (cls.root / "config/neuron.json").write_text(json.dumps({ + "ip": "127.0.0.1", "port": port, "disable_auth": 0, + "sub_filter_error": 0, + })) + cls.log = (cls.root / "server.log").open("w") + cls.start() + + @classmethod + def start(cls): + cls.token = None + env = dict(os.environ) + env["LD_LIBRARY_PATH"] = str(BUILD) + ":" + env.get("LD_LIBRARY_PATH", "/usr/local/lib") + server_log = cls.root / "logs/neuron.log" + log_offset = server_log.stat().st_size if server_log.exists() else 0 + cls.process = subprocess.Popen([ + str(BUILD / "neuron"), "--config_file", "config/neuron.json", + "--config_dir", "config", "--plugin_dir", "plugins", + ], cwd=cls.root, env=env, stdout=cls.log, stderr=subprocess.STDOUT) + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + if cls.process.poll() is not None: + cls.failed = True + raise RuntimeError(f"Neuron exited; see {cls.root / 'server.log'}") + try: + if not server_log.exists() or "manager start" not in server_log.read_text()[log_offset:]: + time.sleep(0.1) + continue + status, body = cls.request("POST", "/login", {"name": "admin", "pass": "0000"}) + if status == 200 and "token" in body: + cls.token = body["token"] + return + except OSError: + pass + time.sleep(0.1) + cls.failed = True + raise RuntimeError(f"Neuron readiness timeout; see {cls.root}") + + @classmethod + def stop(cls): + if cls.process is not None: + if cls.process.poll() is None: + cls.process.send_signal(signal.SIGTERM) + try: + cls.process.wait(timeout=15) + except subprocess.TimeoutExpired: + cls.process.kill() + cls.process.wait(timeout=5) + cls.process = None + + @classmethod + def cleanup(cls): + cls.stop() + if hasattr(cls, "log"): + cls.log.close() + if not cls.failed and not KEEP_RUNTIME: + shutil.rmtree(cls.root) + + @classmethod + def request(cls, method, path, body=None): + headers = {"Content-Type": "application/json"} + if cls.token: + headers["Authorization"] = "Bearer " + cls.token + req = urllib.request.Request( + cls.base + path, method=method, headers=headers, + data=None if body is None else json.dumps(body).encode()) + try: + with urllib.request.urlopen(req, timeout=10) as response: + return response.status, json.loads(response.read()) + except urllib.error.HTTPError as error: + return error.code, json.loads(error.read()) + + def http(self, method, path, body=None, *, status=200, error=None): + actual_status, response = self.request(method, path, body) + self.assertEqual(status, actual_status, (method, path, response)) + if error is not None: + self.assertEqual(error, response.get("error"), response) + return response + + def setUp(self): + self.node = self._testMethodName + self.record = self.root / (self.node + ".jsonl") + self.http("POST", "/node", {"name": self.node, "plugin": "auto_type", + "params": {"hold": True}}, error=0) + self.addCleanup(self.finish_node) + self.http("POST", "/group", {"node": self.node, "group": "g", "interval": 100}, error=0) + state = self.http("GET", "/node/state?node=" + self.node) + if state["running"] != 3: + self.http("POST", "/node/ctl", {"node": self.node, "cmd": 0}, error=0) + + def finish_node(self): + result = self._outcome.result + if any(test is self or getattr(test, "test_case", None) is self + for test, _ in result.failures + result.errors): + type(self).failed = True + self.http("DELETE", "/node", {"name": self.node}, error=0) + + def control(self, **params): + settings = {"hold": False, "type": 3, "value": 123, "error": 0, + "update_mode": "update", "record_path": str(self.record)} + settings.update(params) + self.http("POST", "/node/setting", {"node": self.node, "params": settings}, error=0) + + def add_tags(self, *tags, group="g", method="POST", error=0, status=200): + return self.http(method, "/tags", {"node": self.node, "group": group, + "tags": list(tags)}, error=error, status=status) + + @staticmethod + def tag(name="a", **fields): + return dict({"name": name, "type": 0, "address": "sample", + "attribute": 3, "precision": 0, "decimal": 0}, **fields) + + def read(self, name="a", *, group="g", paginate=False, sync=False): + response = self.http("POST", "/read/paginate" if paginate else "/read", + {"node": self.node, "group": group, "sync": sync}) + return next(tag for tag in response["items" if paginate else "tags"] + if tag["name"] == name) + + def await_sample(self, value=None, error=None, **kwargs): + deadline = time.monotonic() + 8 + actual = None + while time.monotonic() < deadline: + actual = self.read(**kwargs) + if error is not None and actual.get("error") == error: + return actual + if error is None and "error" not in actual and actual.get("value") == value: + return actual + time.sleep(0.05) + self.fail(f"sample did not become value={value!r}, error={error!r}: {actual}") + + def write(self, value, *, name="a", error=0): + status, response = self.request("POST", "/write", { + "node": self.node, "group": "g", "tag": name, "value": value}) + self.assertEqual(error, response.get("error"), (status, response)) + if not error: + self.assertEqual(200, status) + + def records(self): + if not self.record.exists(): + return [] + return [json.loads(line) for line in self.record.read_text().splitlines()] + + def metadata_is_auto(self): + tags = self.http("GET", f"/tags?node={self.node}&group=g")["tags"] + self.assertTrue(tags) + self.assertTrue(all(tag["type"] == 0 for tag in tags), tags) + exported = self.http("GET", "/global/drivers?name=" + self.node) + self.assertTrue(all(tag["type"] == 0 for node in exported["nodes"] + for group in node["groups"] for tag in group["tags"])) + with sqlite3.connect(f"file:{self.root}/persistence/sqlite.db?mode=ro", uri=True) as db: + rows = db.execute("SELECT type FROM tags WHERE driver_name = ?", (self.node,)).fetchall() + self.assertTrue(rows) + self.assertTrue(all(row == (0,) for row in rows), rows) + + def test_resolution_writes_mismatch_recovery_and_restart(self): + self.add_tags(self.tag(), self.tag("b")) + self.control(hold=True) + self.await_sample(error=3011) + self.write(123, error=3011) + self.assertEqual([], self.records()) + self.control() + self.await_sample(123) + self.await_sample(123, paginate=True) + self.await_sample(123, sync=True) + self.metadata_is_auto() + self.write(321) + self.assertEqual({"tag": "a", "type": 3, "value": 321, "path": "single"}, self.records()[-1]) + for value, code in ((40000, 3020), ("123", 3007)): + self.write(value, error=code) + self.assertEqual(1, len(self.records())) + self.http("POST", "/write/tags", {"node": self.node, "group": "g", "tags": [ + {"tag": "a", "value": 111}, {"tag": "b", "value": 222}]}, error=0) + self.assertEqual([("a", 3, 111, "batch"), ("b", 3, 222, "batch")], + [(r["tag"], r["type"], r["value"], r["path"]) for r in self.records()[-2:]]) + self.control(type=13, value="wrong") + self.await_sample(error=3007) + self.write(444) + self.assertEqual(3, self.records()[-1]["type"]) + self.control(error=3002) + self.await_sample(error=3002) + self.control(value=124) + self.await_sample(124) + self.metadata_is_auto() + self.control(hold=True) + self.stop() + self.start() + self.await_sample(error=3011) + self.write(1, error=3011) + self.metadata_is_auto() + self.control(type=13, value="new lifecycle") + self.await_sample("new lifecycle") + self.metadata_is_auto() + + def test_all_update_callbacks(self): + for index, mode in enumerate(("update", "meta", "trace", "im", "im_f_m")): + with self.subTest(mode=mode): + self.control(hold=True, update_mode=mode) + name = f"mode{index}" + self.add_tags(self.tag(name)) + self.await_sample(error=3011, name=name) + self.control(update_mode=mode) + self.await_sample(123, name=name) + self.control(type=10, value=1.5, update_mode=mode) + self.await_sample(error=3007, name=name) + self.control(value=125, update_mode=mode) + self.await_sample(125, name=name) + + def test_scaled_value_and_byte_order(self): + self.add_tags(self.tag(decimal=0.5, bias=1), self.tag("big", address="sample#B")) + self.control(value=256) + self.await_sample(129) + self.await_sample(1, name="big") + self.write(51) + self.assertEqual(100, self.records()[-1]["value"]) + self.write(1, name="big") + self.assertEqual(256, self.records()[-1]["value"]) + self.metadata_is_auto() + + def test_pointer_string_recovery(self): + self.add_tags(self.tag()) + self.control(type=13, value="hello", pointer=True) + self.await_sample("hello") + self.await_sample("hello", paginate=True) + self.write("world") + self.assertEqual({"tag": "a", "type": 13, "value": "world", "path": "single"}, self.records()[-1]) + self.control(type=13, value="recover", pointer=True, error=3002) + self.await_sample(error=3002) + self.control(type=13, value="recover", pointer=True) + self.await_sample("recover") + self.control(type=3) + self.await_sample(error=3007) + self.control(type=13, value="back", pointer=False) + self.await_sample("back") + self.metadata_is_auto() + + def test_batch_validation_and_cross_group_writes(self): + self.add_tags(self.tag()) + self.http("POST", "/group", {"node": self.node, "group": "g2", "interval": 100}, error=0) + self.add_tags(self.tag("b"), group="g2") + self.control(hold=True) + self.await_sample(error=3011) + self.await_sample(error=3011, name="b", group="g2") + for endpoint, body in ( + ("/write/tags", {"node": self.node, "group": "g", "tags": [{"tag": "a", "value": 12}]}), + ("/write/gtags", {"node": self.node, "groups": [ + {"group": "g", "tags": [{"tag": "a", "value": 12}]}, + {"group": "g2", "tags": [{"tag": "b", "value": 13}]}]}), + ): + with self.subTest(endpoint=endpoint): + self.http("POST", endpoint, body, error=3011) + self.assertEqual([], self.records()) + self.control() + self.await_sample(123) + self.await_sample(123, name="b", group="g2") + for value, code in ((40000, 3020), ("123", 3007)): + for endpoint, body in ( + ("/write/tags", {"node": self.node, "group": "g", "tags": [{"tag": "a", "value": value}]}), + ("/write/gtags", {"node": self.node, "groups": [ + {"group": "g", "tags": [{"tag": "a", "value": value}]}]}), + ): + with self.subTest(endpoint=endpoint, value=value): + self.http("POST", endpoint, body, status=400, error=code) + self.assertEqual([], self.records()) + self.http("POST", "/write/gtags", {"node": self.node, "groups": [ + {"group": "g", "tags": [{"tag": "a", "value": 12}]}, + {"group": "g2", "tags": [{"tag": "b", "value": 13}]}]}, error=0) + self.assertEqual({("a", 3, 12), ("b", 3, 13)}, + {(r["tag"], r["type"], r["value"]) for r in self.records()}) + + def test_reconfigure_resets_detection(self): + self.add_tags(self.tag()) + self.control() + self.await_sample(123) + self.control(hold=True) + self.add_tags(self.tag(address="changed"), method="PUT") + self.await_sample(error=3011) + self.write(123, error=3011) + self.control(type=10, value=12.5) + self.await_sample(12.5) + self.write(2.5) + self.assertEqual(10, self.records()[-1]["type"]) + self.assertEqual(2.5, self.records()[-1]["value"]) + self.metadata_is_auto() + + def test_auto_admission_and_explicit_type_regression(self): + self.add_tags(self.tag(attribute=2), error=2204, status=400) + self.add_tags(self.tag(type=3)) + self.control(value=-123) + self.await_sample(-123) + self.write(123) + self.assertEqual(3, self.records()[-1]["type"]) + self.write(40000, error=3020) + self.write("123", error=3007) + self.http("POST", "/node", {"name": "modbus-auto-reject", "plugin": "Modbus TCP"}, error=0) + try: + self.http("POST", "/group", {"node": "modbus-auto-reject", "group": "g", "interval": 100}, error=0) + self.http("POST", "/tags", {"node": "modbus-auto-reject", "group": "g", + "tags": [self.tag(address="1!400001")]}, status=400, error=2204) + finally: + self.http("DELETE", "/node", {"name": "modbus-auto-reject"}, error=0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build", type=Path, default=BUILD) + parser.add_argument("--keep-runtime", action="store_true") + args, remaining = parser.parse_known_args() + BUILD = args.build.resolve() + KEEP_RUNTIME = args.keep_runtime + unittest.main(argv=[__file__] + remaining, verbosity=2) diff --git a/tests/ft/neuron/api.py b/tests/ft/neuron/api.py index fe6a2fbed..148ea1712 100644 --- a/tests/ft/neuron/api.py +++ b/tests/ft/neuron/api.py @@ -107,6 +107,25 @@ def get_nodes_by_tags(type, tags, jwt=config.default_jwt): return requests.get(url=config.BASE_URL + '/api/v2/node', headers={"Authorization": jwt}, params={"type": type, "node": tags}) +def get_node_properties(node, jwt=config.default_jwt): + return requests.get(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, params={"node": node}) + + +@gen_check +def add_node_properties(node, properties, jwt=config.default_jwt): + return requests.post(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "properties": properties}) + + +@gen_check +def update_node_property(node, key, value, jwt=config.default_jwt): + return requests.put(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "key": key, "value": value}) + + +@gen_check +def delete_node_properties(node, keys, jwt=config.default_jwt): + return requests.delete(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "keys": keys}) + + @gen_check def node_ctl(node, ctl, jwt=config.default_jwt): return requests.post(url=config.BASE_URL + '/api/v2/node/ctl', headers={"Authorization": jwt}, json={"node": node, "cmd": ctl}) diff --git a/tests/ft/neuron/error.py b/tests/ft/neuron/error.py index 038bb835a..ab45ba151 100644 --- a/tests/ft/neuron/error.py +++ b/tests/ft/neuron/error.py @@ -36,6 +36,8 @@ NEU_ERR_NODE_TAGS_TOO_LONG = 2016 NEU_ERR_NODE_TAGS_TOO_MANY = 2017 NEU_ERR_NODE_TAGS_INVALID = 2018 +NEU_ERR_NODE_PROPERTY_EXIST = 2019 +NEU_ERR_NODE_PROPERTY_NOT_EXIST = 2020 NEU_ERR_GROUP_ALREADY_SUBSCRIBED = 2101 NEU_ERR_GROUP_NOT_SUBSCRIBE = 2102 diff --git a/tests/ft/node/test_node_property.py b/tests/ft/node/test_node_property.py new file mode 100644 index 000000000..a2c211371 --- /dev/null +++ b/tests/ft/node/test_node_property.py @@ -0,0 +1,268 @@ +import math + +import pytest +import requests + +import neuron.api as api +import neuron.config as config +from neuron.common import class_setup_and_teardown, case_time +from neuron.error import ( + NEU_ERR_BODY_IS_WRONG, + NEU_ERR_NEED_TOKEN, + NEU_ERR_NODE_NOT_EXIST, + NEU_ERR_NODE_PROPERTY_EXIST, + NEU_ERR_NODE_PROPERTY_NOT_EXIST, + NEU_ERR_PARAM_IS_WRONG, +) + + +VALUES = { + "string": "hello", + "empty-string": "", + "true": True, + "false": False, + "zero": 0, + "integer-min": -(2 ** 63), + "integer-max": 2 ** 63 - 1, + "large-integer": 2 ** 53 + 1, + "float": 1.25, + "float-zero": 0.0, + "negative-zero": -0.0, + "float-max": 1.7976931348623157e308, + "float-min": 5e-324, + "strings": ["", "text", "中文"], + "booleans": [False, True], + "integers": [-(2 ** 63), 0, 2 ** 63 - 1], + "floats": [0.0, -1.5, 2.5], + "numbers": [0, 1.5, -2, 0.0], + "empty-array": [], + "转义\"\\\n": "中文\"\\\n\r\t\b\f\u0000end", + "nul-array": ["\u0000", "a\u0000b"], +} + + +def assert_error(response, status, code): + assert response.status_code == status, response.text + assert response.json() == {"error": code} + + +def assert_value(actual, expected): + assert type(actual) is type(expected) + assert actual == expected + if isinstance(expected, list): + for actual_item, expected_item in zip(actual, expected): + assert_value(actual_item, expected_item) + elif isinstance(expected, float) and expected == 0: + assert math.copysign(1, actual) == math.copysign(1, expected) + + +def assert_properties(node, expected): + response = api.get_node_properties(node) + assert response.status_code == 200, response.text + assert set(response.json()) == {"properties"} + properties = response.json()["properties"] + assert len(properties) == len(expected) + actual = {} + for item in properties: + assert set(item) == {"key", "value"} + assert item["key"] not in actual + actual[item["key"]] = item["value"] + assert actual.keys() == expected.keys() + for key, value in expected.items(): + assert_value(actual[key], value) + + +@pytest.fixture(params=[config.PLUGIN_MODBUS_TCP, config.PLUGIN_MQTT], + ids=["southbound", "northbound"]) +def property_node(request, class_setup_and_teardown): + node = "property-driver" if request.param == config.PLUGIN_MODBUS_TCP else "property-app" + api.add_node_check(node, request.param) + state = {"name": node, "plugin": request.param} + try: + yield state + finally: + api.del_node_check(state["name"]) + + +class TestNodeProperty: + def test_crud_and_value_types(self, property_node): + node = property_node["name"] + assert_properties(node, {}) + api.add_node_properties_check( + node, [{"key": key, "value": value} for key, value in VALUES.items()]) + assert_properties(node, VALUES) + + for value in VALUES.values(): + api.update_node_property_check(node, "string", value) + assert_properties(node, dict(VALUES, string=value)) + api.update_node_property_check(node, "string", VALUES["string"]) + + api.delete_node_properties_check(node, ["false", "zero", "absent"]) + remaining = {key: value for key, value in VALUES.items() + if key not in ("false", "zero")} + assert_properties(node, remaining) + api.delete_node_properties_check(node, ["absent"]) + assert_properties(node, remaining) + api.delete_node_properties_check(node, list(remaining)) + assert_properties(node, {}) + api.add_node_properties_check(node, [{"key": "zero", "value": 0}]) + assert_properties(node, {"zero": 0}) + + @pytest.mark.parametrize("conflict_first", [False, True]) + def test_post_conflict_is_atomic(self, property_node, conflict_first): + node = property_node["name"] + api.add_node_properties_check(node, [{"key": "existing", "value": False}]) + properties = [{"key": "new", "value": 0}, + {"key": "existing", "value": "replacement"}] + if conflict_first: + properties.reverse() + assert_error(api.add_node_properties(node, properties), + 409, NEU_ERR_NODE_PROPERTY_EXIST) + assert_properties(node, {"existing": False}) + assert_error(api.update_node_property(node, "missing", 0), + 404, NEU_ERR_NODE_PROPERTY_NOT_EXIST) + assert_properties(node, {"existing": False}) + + def test_missing_node(self, property_node): + node = property_node["name"] + "-missing" + responses = [ + api.get_node_properties(node), + api.add_node_properties(node, [{"key": "key", "value": 0}]), + api.update_node_property(node, "key", False), + api.delete_node_properties(node, ["missing"]), + ] + for response in responses: + assert_error(response, 404, NEU_ERR_NODE_NOT_EXIST) + + def test_rename_delete_and_recreate(self, property_node): + old_name = property_node["name"] + values = {"false": False, "zero": 0, "empty": [], "escaped": "a\u0000b"} + api.add_node_properties_check( + old_name, [{"key": key, "value": value} for key, value in values.items()]) + new_name = old_name + "-renamed" + api.update_node_check(old_name, new_name) + property_node["name"] = new_name + assert_error(api.get_node_properties(old_name), 404, NEU_ERR_NODE_NOT_EXIST) + assert_properties(new_name, values) + api.update_node_property_check(new_name, "false", True) + values["false"] = True + api.delete_node_properties_check(new_name, ["zero"]) + del values["zero"] + assert_properties(new_name, values) + + api.add_node_check(old_name, property_node["plugin"]) + try: + assert_properties(old_name, {}) + finally: + api.del_node_check(old_name) + api.del_node_check(new_name) + assert_error(api.get_node_properties(new_name), 404, NEU_ERR_NODE_NOT_EXIST) + api.add_node_check(new_name, property_node["plugin"]) + assert_properties(new_name, {}) + api.add_node_properties_check(new_name, [{"key": "false", "value": False}]) + assert_properties(new_name, {"false": False}) + + def test_restart_persistence(self, property_node, class_setup_and_teardown): + node = property_node["name"] + api.add_node_properties_check( + node, [{"key": key, "value": value} for key, value in VALUES.items()]) + api.update_node_property_check(node, "string", False) + api.delete_node_properties_check(node, ["zero", "absent"]) + new_name = node + "-persisted" + api.update_node_check(node, new_name) + property_node["name"] = new_name + class_setup_and_teardown.restart() + expected = dict(VALUES, string=False) + del expected["zero"] + assert_properties(new_name, expected) + assert_error(api.get_node_properties(node), 404, NEU_ERR_NODE_NOT_EXIST) + + api.del_node_check(new_name) + api.add_node_check(new_name, property_node["plugin"]) + class_setup_and_teardown.restart() + assert_properties(new_name, {}) + + @pytest.mark.parametrize("value", [None, {}, {"a": 1}, [[1]], [None], [{}], + [1, "1"], [True, 1], [False, 0.0], + ["true", True], ["", []]]) + def test_invalid_value_is_atomic(self, property_node, value): + node = property_node["name"] + api.add_node_properties_check(node, [{"key": "existing", "value": False}]) + assert_error(api.add_node_properties(node, [ + {"key": "new", "value": 0}, {"key": "bad", "value": value}]), + 400, NEU_ERR_BODY_IS_WRONG) + assert_error(api.update_node_property(node, "existing", value), + 400, NEU_ERR_BODY_IS_WRONG) + assert_properties(node, {"existing": False}) + + @pytest.mark.parametrize("method,fields", [ + ("POST", {}), + ("POST", {"properties": []}), + ("POST", {"properties": None}), + ("POST", {"properties": {}}), + ("POST", {"properties": [None]}), + ("POST", {"properties": [{"value": 1}]}), + ("POST", {"properties": [{"key": "", "value": 1}]}), + ("POST", {"properties": [{"key": "bad\u0000key", "value": 1}]}), + ("POST", {"properties": [{"key": 1, "value": 1}]}), + ("POST", {"properties": [{"key": "bad"}]}), + ("POST", {"properties": [{"key": "new", "value": 1}, + {"key": "new", "value": 2}]}), + ("PUT", {}), + ("PUT", {"key": "existing"}), + ("PUT", {"value": False}), + ("PUT", {"key": "", "value": False}), + ("PUT", {"key": "bad\u0000key", "value": False}), + ("PUT", {"key": 1, "value": False}), + ("DELETE", {}), + ("DELETE", {"keys": []}), + ("DELETE", {"keys": None}), + ("DELETE", {"keys": "existing"}), + ("DELETE", {"keys": ["existing", "existing"]}), + ("DELETE", {"keys": ["existing", ""]}), + ("DELETE", {"keys": ["existing", "bad\u0000key"]}), + ("DELETE", {"keys": ["existing", None]}), + ("DELETE", {"keys": ["existing", 1]}), + ]) + def test_invalid_request_is_atomic(self, property_node, method, fields): + node = property_node["name"] + api.add_node_properties_check(node, [{"key": "existing", "value": False}]) + body = dict(fields, node=node) + if (method == "POST" and isinstance(body.get("properties"), list) + and body["properties"]): + body["properties"] = [{"key": "valid", "value": 0}] + body["properties"] + response = requests.request( + method, config.BASE_URL + "/api/v2/node/properties", + headers={"Authorization": config.default_jwt}, json=body) + assert_error(response, 400, NEU_ERR_BODY_IS_WRONG) + assert_properties(node, {"existing": False}) + + @pytest.mark.parametrize("node", [None, "", 1, "bad\u0000node"]) + def test_invalid_node(self, property_node, node): + for response in [ + api.add_node_properties(node, [{"key": "key", "value": 0}]), + api.update_node_property(node, "key", False), + api.delete_node_properties(node, ["key"]), + ]: + assert_error(response, 400, NEU_ERR_BODY_IS_WRONG) + assert_properties(property_node["name"], {}) + + @pytest.mark.parametrize("params", [{}, {"node": ""}, {"node": "bad\u0000node"}]) + def test_invalid_query(self, property_node, params): + response = requests.get( + config.BASE_URL + "/api/v2/node/properties", + headers={"Authorization": config.default_jwt}, params=params) + assert_error(response, 400, NEU_ERR_PARAM_IS_WRONG) + assert_properties(property_node["name"], {}) + + @pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"]) + def test_jwt_required(self, property_node, method): + node = property_node["name"] + body = {"node": node, "properties": [{"key": "key", "value": 0}], + "key": "key", "value": False, "keys": ["key"]} + response = requests.request( + method, config.BASE_URL + "/api/v2/node/properties", + params={"node": node} if method == "GET" else None, + json=body if method != "GET" else None) + assert_error(response, 401, NEU_ERR_NEED_TOKEN) + assert_properties(node, {}) diff --git a/tests/plugins/auto_type/CMakeLists.txt b/tests/plugins/auto_type/CMakeLists.txt new file mode 100644 index 000000000..2c8f00915 --- /dev/null +++ b/tests/plugins/auto_type/CMakeLists.txt @@ -0,0 +1,7 @@ +set(LIBRARY_OUTPUT_PATH "${CMAKE_BINARY_DIR}/tests/plugins") +set(CMAKE_BUILD_RPATH ./) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/auto_type.json + DESTINATION ${CMAKE_BINARY_DIR}/tests/plugins/schema/) +add_library(plugin-auto-type SHARED auto_type.c) +target_include_directories(plugin-auto-type PRIVATE ${CMAKE_SOURCE_DIR}/include/neuron) +target_link_libraries(plugin-auto-type neuron-base) diff --git a/tests/plugins/auto_type/auto_type.c b/tests/plugins/auto_type/auto_type.c new file mode 100644 index 000000000..8a02fefca --- /dev/null +++ b/tests/plugins/auto_type/auto_type.c @@ -0,0 +1,362 @@ +#include +#include + +#include "errcodes.h" +#include + +struct neu_plugin { + neu_plugin_common_t common; + pthread_mutex_t lock; + json_t * settings; +}; + +static neu_plugin_t *mock_open(void) +{ + neu_plugin_t *plugin = calloc(1, sizeof(*plugin)); + if (plugin == NULL) { + return NULL; + } + neu_plugin_common_init(&plugin->common); + pthread_mutex_init(&plugin->lock, NULL); + plugin->settings = json_object(); + return plugin; +} + +static int mock_close(neu_plugin_t *plugin) +{ + json_decref(plugin->settings); + pthread_mutex_destroy(&plugin->lock); + free(plugin); + return 0; +} + +static int mock_init(neu_plugin_t *plugin, bool load) +{ + (void) plugin; + (void) load; + return 0; +} + +static int mock_noop(neu_plugin_t *plugin) +{ + (void) plugin; + return 0; +} + +static bool sample_type(int type) +{ + return (type >= NEU_TYPE_INT8 && type <= NEU_TYPE_LWORD && + type != NEU_TYPE_ERROR); +} + +static int mock_setting(neu_plugin_t *plugin, const char *config) +{ + json_t *root = json_loads(config, 0, NULL); + json_t *settings = json_object_get(root, "params"); + if (!json_is_object(settings)) { + json_decref(root); + return NEU_ERR_NODE_SETTING_INVALID; + } + json_incref(settings); + json_decref(root); + json_t * type = json_object_get(settings, "type"); + json_t * mode = json_object_get(settings, "update_mode"); + const char *m = json_string_value(mode); + if ((type && + (!json_is_integer(type) || !sample_type(json_integer_value(type)))) || + (mode && + (!m || + (strcmp(m, "update") && strcmp(m, "meta") && strcmp(m, "trace") && + strcmp(m, "im") && strcmp(m, "im_f_m"))))) { + json_decref(settings); + return NEU_ERR_NODE_SETTING_INVALID; + } + pthread_mutex_lock(&plugin->lock); + json_decref(plugin->settings); + plugin->settings = settings; + pthread_mutex_unlock(&plugin->lock); + return 0; +} + +static int mock_validate(neu_plugin_t *plugin, neu_datatag_t *tag) +{ + (void) plugin; + return tag->type == NEU_TYPE_AUTO || sample_type(tag->type) + ? 0 + : NEU_ERR_TAG_TYPE_NOT_SUPPORT; +} + +static int mock_validator(const neu_datatag_t *tag) +{ + (void) tag; + return 0; +} + +static neu_dvalue_t sample(json_t *settings) +{ + json_t * jtype = json_object_get(settings, "type"); + json_t * jvalue = json_object_get(settings, "value"); + int error = json_integer_value(json_object_get(settings, "error")); + neu_dvalue_t v = { .type = + jtype ? json_integer_value(jtype) : NEU_TYPE_INT16 }; + int64_t i = jvalue ? json_integer_value(jvalue) : 123; + double d = jvalue ? json_number_value(jvalue) : 123; + const char * s = json_string_value(jvalue); + if (!s) { + s = "sample"; + } + if (error) { + v.type = NEU_TYPE_ERROR; + v.value.i32 = error; + return v; + } + switch (v.type) { + case NEU_TYPE_INT8: + v.value.i8 = i; + break; + case NEU_TYPE_UINT8: + case NEU_TYPE_BIT: + v.value.u8 = i; + break; + case NEU_TYPE_INT16: + v.value.i16 = i; + break; + case NEU_TYPE_UINT16: + case NEU_TYPE_WORD: + v.value.u16 = i; + break; + case NEU_TYPE_INT32: + v.value.i32 = i; + break; + case NEU_TYPE_UINT32: + case NEU_TYPE_DWORD: + v.value.u32 = i; + break; + case NEU_TYPE_INT64: + v.value.i64 = i; + break; + case NEU_TYPE_UINT64: + case NEU_TYPE_LWORD: + v.value.u64 = i; + break; + case NEU_TYPE_FLOAT: + v.value.f32 = d; + break; + case NEU_TYPE_DOUBLE: + v.value.d64 = d; + break; + case NEU_TYPE_BOOL: + v.value.boolean = json_is_true(jvalue); + break; + case NEU_TYPE_STRING: + if (json_is_true(json_object_get(settings, "pointer"))) { + v.value.ptr.type = NEU_TYPE_STRING; + v.value.ptr.length = strlen(s) + 1; + v.value.ptr.ptr = (uint8_t *) strdup(s); + v.type = NEU_TYPE_PTR; + } else { + snprintf(v.value.str, sizeof(v.value.str), "%s", s); + } + break; + case NEU_TYPE_BYTES: { + size_t n = json_is_array(jvalue) ? json_array_size(jvalue) : 0; + if (n > UINT8_MAX) { + n = UINT8_MAX; + } + if (json_is_true(json_object_get(settings, "pointer"))) { + v.value.ptr.type = NEU_TYPE_BYTES; + v.value.ptr.length = n; + v.value.ptr.ptr = calloc(n ? n : 1, 1); + for (size_t k = 0; k < n; k++) { + v.value.ptr.ptr[k] = + json_integer_value(json_array_get(jvalue, k)); + } + v.type = NEU_TYPE_PTR; + } else { + v.value.bytes.length = n; + for (size_t k = 0; k < n; k++) { + v.value.bytes.bytes[k] = + json_integer_value(json_array_get(jvalue, k)); + } + } + break; + } + default: + break; + } + json_t *quality = json_object_get(settings, "quality"); + json_t *timestamp = json_object_get(settings, "timestamp"); + v.attrs.has_quality = quality != NULL; + v.attrs.quality = json_integer_value(quality); + v.attrs.has_timestamp = timestamp != NULL; + v.attrs.timestamp = json_integer_value(timestamp); + return v; +} + +static int mock_timer(neu_plugin_t *plugin, neu_plugin_group_t *group) +{ + pthread_mutex_lock(&plugin->lock); + json_t *settings = json_deep_copy(plugin->settings); + pthread_mutex_unlock(&plugin->lock); + json_t *hold = json_object_get(settings, "hold"); + if (!hold || !json_is_false(hold)) { + json_decref(settings); + return 0; + } + const char *mode = + json_string_value(json_object_get(settings, "update_mode")); + const adapter_callbacks_t *cb = plugin->common.adapter_callbacks; + neu_adapter_t * adapter = plugin->common.adapter; + utarray_foreach(group->tags, neu_datatag_t *, tag) + { + if (!(tag->attribute & NEU_ATTRIBUTE_READ)) { + continue; + } + neu_dvalue_t v = sample(settings); + if (mode && !strcmp(mode, "meta")) { + cb->driver.update_with_meta(adapter, group->group_name, tag->name, + v, NULL, 0); + } else if (mode && !strcmp(mode, "trace")) { + cb->driver.update_with_trace(adapter, group->group_name, tag->name, + v, NULL, 0, NULL); + } else if (mode && !strcmp(mode, "im")) { + cb->driver.update_im(adapter, group->group_name, tag->name, v, NULL, + 0); + } else if (mode && !strcmp(mode, "im_f_m")) { + cb->driver.update_im_f_m(adapter, group->group_name, tag->name, v, + NULL, 0); + } else { + cb->driver.update(adapter, group->group_name, tag->name, v); + } + if (v.type == NEU_TYPE_PTR) { + free(v.value.ptr.ptr); + } + } + json_decref(settings); + return 0; +} + +static json_t *write_value(neu_type_e type, neu_value_u value) +{ + switch (type) { + case NEU_TYPE_INT8: + return json_integer(value.i8); + case NEU_TYPE_UINT8: + case NEU_TYPE_BIT: + return json_integer(value.u8); + case NEU_TYPE_INT16: + return json_integer(value.i16); + case NEU_TYPE_UINT16: + case NEU_TYPE_WORD: + return json_integer(value.u16); + case NEU_TYPE_INT32: + return json_integer(value.i32); + case NEU_TYPE_UINT32: + case NEU_TYPE_DWORD: + return json_integer(value.u32); + case NEU_TYPE_INT64: + return json_integer(value.i64); + case NEU_TYPE_UINT64: + case NEU_TYPE_LWORD: + return json_integer(value.u64); + case NEU_TYPE_FLOAT: + return json_real(value.f32); + case NEU_TYPE_DOUBLE: + return json_real(value.d64); + case NEU_TYPE_BOOL: + return json_boolean(value.boolean); + case NEU_TYPE_STRING: + return json_string(value.str); + case NEU_TYPE_BYTES: { + json_t *array = json_array(); + for (unsigned i = 0; i < value.bytes.length; i++) { + json_array_append_new(array, json_integer(value.bytes.bytes[i])); + } + return array; + } + default: + return json_null(); + } +} + +static int record_write(neu_plugin_t *plugin, neu_datatag_t *tag, + neu_value_u value, const char *path) +{ + int error = 0; + pthread_mutex_lock(&plugin->lock); + const char *file = + json_string_value(json_object_get(plugin->settings, "record_path")); + if (file) { + FILE *out = fopen(file, "a"); + if (!out) { + error = NEU_ERR_EINTERNAL; + } else { + json_t *record = json_pack("{s:s,s:i,s:s}", "tag", tag->name, + "type", tag->type, "path", path); + json_object_set_new(record, "value", write_value(tag->type, value)); + if (json_dumpf(record, out, JSON_COMPACT) || + fputc('\n', out) == EOF) { + error = NEU_ERR_EINTERNAL; + } + if (fclose(out)) { + error = NEU_ERR_EINTERNAL; + } + json_decref(record); + } + } + pthread_mutex_unlock(&plugin->lock); + return error; +} + +static int mock_write(neu_plugin_t *plugin, void *req, neu_datatag_t *tag, + neu_value_u value) +{ + int error = record_write(plugin, tag, value, "single"); + plugin->common.adapter_callbacks->driver.write_response( + plugin->common.adapter, req, error); + return 0; +} + +static int mock_writes(neu_plugin_t *plugin, void *req, UT_array *tags) +{ + int error = 0; + utarray_foreach(tags, neu_plugin_tag_value_t *, item) + { + int current = record_write(plugin, item->tag, item->value, "batch"); + if (current) { + error = current; + } + } + plugin->common.adapter_callbacks->driver.write_response( + plugin->common.adapter, req, error); + return 0; +} + +static const neu_plugin_intf_funs_t functions = { + .open = mock_open, + .close = mock_close, + .init = mock_init, + .uninit = mock_noop, + .start = mock_noop, + .stop = mock_noop, + .setting = mock_setting, + .driver.validate_tag = mock_validate, + .driver.tag_validator = mock_validator, + .driver.group_timer = mock_timer, + .driver.group_sync = mock_timer, + .driver.write_tag = mock_write, + .driver.write_tags = mock_writes, +}; + +const neu_plugin_module_t neu_plugin_module = { + .version = NEURON_PLUGIN_VER_1_0, + .schema = "auto_type", + .module_name = "auto_type", + .module_descr = "Test-only AUTO type driver", + .module_descr_zh = "仅测试使用的 AUTO 类型驱动", + .intf_funs = &functions, + .kind = NEU_PLUGIN_KIND_CUSTOM, + .type = NEU_NA_TYPE_DRIVER, + .display = true, + .single = false, +}; diff --git a/tests/plugins/auto_type/auto_type.json b/tests/plugins/auto_type/auto_type.json new file mode 100644 index 000000000..355c59316 --- /dev/null +++ b/tests/plugins/auto_type/auto_type.json @@ -0,0 +1,4 @@ +{ + "tag_regex": [{"type": 0, "regex": ""}], + "group_interval": 100 +} diff --git a/tests/plugins/c1/c1_2.c b/tests/plugins/c1/c1_2.c index bb3fc104f..02df6fb90 100644 --- a/tests/plugins/c1/c1_2.c +++ b/tests/plugins/c1/c1_2.c @@ -63,7 +63,7 @@ static const neu_plugin_intf_funs_t plugin_intf_funs = { }; const neu_plugin_module_t neu_plugin_module = { - .version = NEU_VERSION(2, 16, 99), + .version = NEU_VERSION(NEU_VERSION_MAJOR, NEU_VERSION_MINOR, 99), .schema = "c1", .module_name = "c1", .module_descr = "c1 v2", diff --git a/tests/ut/CMakeLists.txt b/tests/ut/CMakeLists.txt index 5d5a0b8fc..1791aaafd 100644 --- a/tests/ut/CMakeLists.txt +++ b/tests/ut/CMakeLists.txt @@ -77,6 +77,14 @@ target_include_directories(common_test PRIVATE ) target_link_libraries(common_test neuron-base gtest_main gtest) +add_executable(cache_test cache_test.cc + ${CMAKE_SOURCE_DIR}/src/adapter/driver/cache.c) +target_include_directories(cache_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/include +) +target_link_libraries(cache_test gtest_main gtest pthread jansson m) + file(COPY ${CMAKE_SOURCE_DIR}/tests/ut/serverBMS_3_test.cid DESTINATION ${UT_DIRECTORY}/config) add_executable(cid_test cid_test.cc) target_include_directories(cid_test PRIVATE @@ -102,7 +110,28 @@ target_include_directories(ede_test PRIVATE ) target_link_libraries(ede_test neuron-base gtest_main gtest) +add_executable(node_property_persist_test node_property_persist_test.cc) +target_include_directories(node_property_persist_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/include +) +target_compile_definitions(node_property_persist_test PRIVATE + NEU_PROPERTY_SCHEMA_DIR="${CMAKE_SOURCE_DIR}/persistence" +) +target_link_libraries(node_property_persist_test neuron-base gtest_main gtest pthread jansson sqlite3) + +add_executable(driver_auto_test driver_auto_test.cc driver_auto_helpers.c + ${CMAKE_SOURCE_DIR}/src/adapter/driver/cache.c) +target_include_directories(driver_auto_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/include +) +target_compile_options(driver_auto_test PRIVATE -ffunction-sections -fdata-sections) +target_link_libraries(driver_auto_test neuron-base gtest_main gtest pthread jansson m + -Wl,--gc-sections) + include(GoogleTest) +gtest_discover_tests(driver_auto_test) gtest_discover_tests(json_test) gtest_discover_tests(http_test) gtest_discover_tests(jwt_test) @@ -113,6 +142,8 @@ gtest_discover_tests(async_queue_test) gtest_discover_tests(rolling_counter_test) gtest_discover_tests(mqtt_client_test) gtest_discover_tests(common_test) +gtest_discover_tests(cache_test) gtest_discover_tests(cid_test) gtest_discover_tests(mqtt_schema_test) gtest_discover_tests(ede_test) +gtest_discover_tests(node_property_persist_test) diff --git a/tests/ut/cache_test.cc b/tests/ut/cache_test.cc new file mode 100644 index 000000000..e8f36eeff --- /dev/null +++ b/tests/ut/cache_test.cc @@ -0,0 +1,1048 @@ +#include + +#include +#include +#include + +extern "C" { +#include "adapter/driver/cache.h" +#include "errcodes.h" + +bool sub_filter_err = false; +} + +class DriverCacheSample : public testing::Test { +protected: + void SetUp() override + { + cache = neu_driver_cache_new(); + neu_dvalue_t initial = {}; + initial.type = NEU_TYPE_ERROR; + neu_driver_cache_add(cache, "group", "tag", initial); + } + + void TearDown() override + { + neu_driver_cache_destroy(cache); + sub_filter_err = false; + } + + neu_dvalue_t sample() + { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_DOUBLE; + value.value.d64 = 1.25; + value.attrs.has_timestamp = true; + value.attrs.timestamp = 100; + value.attrs.has_quality = true; + value.attrs.quality = 192; + value.attrs.has_source_timestamp = true; + value.attrs.source_timestamp = INT64_C(1712345678901); + value.attrs.has_server_timestamp = true; + value.attrs.server_timestamp = -INT64_C(1712345678902); + return value; + } + + void expect_attrs(const neu_tag_sample_attrs_t &expected, + const neu_tag_sample_attrs_t &actual) + { + EXPECT_EQ(expected.has_timestamp, actual.has_timestamp); + EXPECT_EQ(expected.timestamp, actual.timestamp); + EXPECT_EQ(expected.has_quality, actual.has_quality); + EXPECT_EQ(expected.quality, actual.quality); + EXPECT_EQ(expected.has_source_timestamp, actual.has_source_timestamp); + EXPECT_EQ(expected.source_timestamp, actual.source_timestamp); + EXPECT_EQ(expected.has_server_timestamp, actual.has_server_timestamp); + EXPECT_EQ(expected.server_timestamp, actual.server_timestamp); + } + + bool update(neu_dvalue_t value) + { + return neu_driver_cache_update_change(cache, "group", "tag", 200, value, + NULL, 0, false); + } + + int changed() + { + return neu_driver_cache_meta_get_changed(cache, "group", "tag", &out, + &metas, &n_meta); + } + + neu_driver_cache_t * cache = NULL; + neu_driver_cache_value_t out = {}; + neu_tag_meta_t * metas = NULL; + int n_meta = 0; +}; + +TEST_F(DriverCacheSample, existing_update_preserves_optional_attrs) +{ + for (int flags = 0; flags < 16; ++flags) { + SCOPED_TRACE(flags); + neu_dvalue_t value = sample(); + value.attrs.has_timestamp = flags & 1; + value.attrs.has_quality = flags & 2; + value.attrs.has_source_timestamp = flags & 4; + value.attrs.has_server_timestamp = flags & 8; + neu_driver_cache_update(cache, "group", "tag", 200, value, NULL, 0); + + ASSERT_EQ(0, + neu_driver_cache_meta_get(cache, "group", "tag", &out, &metas, + &n_meta)); + EXPECT_EQ(200, out.timestamp); + value.attrs.timestamp = flags & 1 ? value.attrs.timestamp : 0; + value.attrs.quality = flags & 2 ? value.attrs.quality : 0; + value.attrs.source_timestamp = + flags & 4 ? value.attrs.source_timestamp : 0; + value.attrs.server_timestamp = + flags & 8 ? value.attrs.server_timestamp : 0; + expect_attrs(value.attrs, out.value.attrs); + } +} + +TEST_F(DriverCacheSample, timestamp_alone_does_not_trigger_change) +{ + neu_dvalue_t value = sample(); + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + struct { + int64_t neu_tag_sample_attrs_t::*value; + bool neu_tag_sample_attrs_t::*present; + } timestamps[] = { + { &neu_tag_sample_attrs_t::timestamp, + &neu_tag_sample_attrs_t::has_timestamp }, + { &neu_tag_sample_attrs_t::source_timestamp, + &neu_tag_sample_attrs_t::has_source_timestamp }, + { &neu_tag_sample_attrs_t::server_timestamp, + &neu_tag_sample_attrs_t::has_server_timestamp }, + }; + for (const auto ×tamp : timestamps) { + auto check = [&] { + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); + ASSERT_EQ(0, + neu_driver_cache_meta_get(cache, "group", "tag", &out, + &metas, &n_meta)); + neu_tag_sample_attrs_t expected = value.attrs; + if (!(expected.*timestamp.present)) { + expected.*timestamp.value = 0; + } + expect_attrs(expected, out.value.attrs); + }; + value.attrs.*timestamp.value += 1; + check(); + value.attrs.*timestamp.value = 0; + check(); + value.attrs.*timestamp.value = 123; + value.attrs.*timestamp.present = false; + check(); + value.attrs.*timestamp.value += 1; + check(); + value.attrs.*timestamp.value = 0; + value.attrs.*timestamp.present = true; + check(); + } +} + +TEST_F(DriverCacheSample, explicit_zero_attrs_are_present) +{ + neu_dvalue_t value = sample(); + value.attrs.timestamp = 0; + value.attrs.quality = 0; + value.attrs.source_timestamp = 0; + value.attrs.server_timestamp = 0; + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); +} + +TEST_F(DriverCacheSample, quality_change_stays_pending_until_read) +{ + for (neu_type_e type : { NEU_TYPE_FLOAT, NEU_TYPE_DOUBLE }) { + neu_dvalue_t value = sample(); + value.type = type; + if (type == NEU_TYPE_FLOAT) { + value.value.f32 = 1.25f; + } + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + value.attrs.quality = 0; + EXPECT_TRUE(update(value)); + value.attrs.timestamp++; + value.attrs.source_timestamp++; + value.attrs.has_server_timestamp = false; + EXPECT_FALSE(update(value)); + ASSERT_EQ(0, changed()); + EXPECT_TRUE(out.value.attrs.has_quality); + EXPECT_EQ(0, out.value.attrs.quality); + EXPECT_EQ(value.attrs.source_timestamp, + out.value.attrs.source_timestamp); + EXPECT_FALSE(out.value.attrs.has_server_timestamp); + EXPECT_EQ(0, out.value.attrs.server_timestamp); + EXPECT_NE(0, changed()); + } +} + +TEST_F(DriverCacheSample, absent_attrs_clear_previous_sample) +{ + neu_dvalue_t value = sample(); + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + value.attrs = {}; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); + + value.attrs.has_quality = true; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); +} + +TEST_F(DriverCacheSample, forced_update_preserves_attrs) +{ + neu_dvalue_t value = sample(); + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + value.attrs.timestamp++; + value.attrs.source_timestamp = 0; + value.attrs.has_server_timestamp = false; + EXPECT_FALSE(neu_driver_cache_update_change(cache, "group", "tag", 300, + value, NULL, 0, true)); + ASSERT_EQ(0, changed()); + EXPECT_EQ(300, out.timestamp); + value.attrs.server_timestamp = 0; + expect_attrs(value.attrs, out.value.attrs); + EXPECT_NE(0, changed()); +} + +TEST_F(DriverCacheSample, errors_and_recovery_replace_attrs) +{ + neu_dvalue_t value = sample(); + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + neu_dvalue_t error = sample(); + error.type = NEU_TYPE_ERROR; + error.value.i32 = 1; + error.attrs.source_timestamp = std::numeric_limits::min(); + error.attrs.server_timestamp = std::numeric_limits::max(); + EXPECT_TRUE(update(error)); + ASSERT_EQ(0, changed()); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + EXPECT_EQ(1, out.value.value.i32); + expect_attrs(error.attrs, out.value.attrs); + + value.attrs = {}; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + EXPECT_EQ(NEU_TYPE_DOUBLE, out.value.type); + expect_attrs(value.attrs, out.value.attrs); + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); + + error.attrs.has_timestamp = false; + error.attrs.has_quality = false; + error.attrs.has_source_timestamp = false; + error.attrs.has_server_timestamp = false; + EXPECT_TRUE(update(error)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); +} + +TEST_F(DriverCacheSample, filtered_error_compares_last_good_quality) +{ + sub_filter_err = true; + neu_dvalue_t value = sample(); + ASSERT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + + neu_dvalue_t error = sample(); + error.type = NEU_TYPE_ERROR; + error.value.i32 = 1; + error.attrs = {}; + EXPECT_FALSE(update(error)); + EXPECT_NE(0, changed()); + ASSERT_EQ(0, + neu_driver_cache_meta_get(cache, "group", "tag", &out, &metas, + &n_meta)); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + expect_attrs(error.attrs, out.value.attrs); + + value.attrs.timestamp++; + value.attrs.source_timestamp = 0; + value.attrs.has_server_timestamp = false; + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); + ASSERT_EQ(0, + neu_driver_cache_meta_get(cache, "group", "tag", &out, &metas, + &n_meta)); + value.attrs.server_timestamp = 0; + expect_attrs(value.attrs, out.value.attrs); + + EXPECT_FALSE(update(error)); + value.attrs.quality = 0; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); + + EXPECT_FALSE(update(error)); + value.attrs.has_source_timestamp = false; + EXPECT_FALSE(update(value)); + EXPECT_NE(0, changed()); + + EXPECT_FALSE(update(error)); + value.attrs.has_quality = false; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); + + EXPECT_FALSE(update(error)); + value.attrs.has_quality = true; + EXPECT_TRUE(update(value)); + ASSERT_EQ(0, changed()); + expect_attrs(value.attrs, out.value.attrs); +} + +TEST_F(DriverCacheSample, copy_dvalue_preserves_attrs) +{ + for (int flags = 0; flags < 16; ++flags) { + SCOPED_TRACE(flags); + neu_dvalue_t src = sample(); + src.attrs.has_timestamp = flags & 1; + src.attrs.has_quality = flags & 2; + src.attrs.has_source_timestamp = flags & 4; + src.attrs.has_server_timestamp = flags & 8; + neu_dvalue_t dst = {}; + neu_copy_dvalue(&src, &dst); + EXPECT_EQ(src.type, dst.type); + EXPECT_EQ(src.value.d64, dst.value.d64); + expect_attrs(src.attrs, dst.attrs); + } +} + +class DriverCacheAuto : public DriverCacheSample { +protected: + void SetUp() override + { + DriverCacheSample::SetUp(); + configure(); + } + + void TearDown() override + { + release(); + DriverCacheSample::TearDown(); + } + + void configure(neu_type_e type = NEU_TYPE_AUTO, uint8_t precision = 0, + double decimal = 0, double bias = 0) + { + neu_datatag_t tag = {}; + tag.name = const_cast("tag"); + tag.type = type; + tag.precision = precision; + tag.decimal = decimal; + tag.bias = bias; + neu_dvalue_t initial = {}; + initial.type = NEU_TYPE_ERROR; + initial.value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY; + neu_driver_cache_add_tag(cache, "group", &tag, initial); + } + + int checked(neu_dvalue_t value, bool force = false) + { + return neu_driver_cache_update_checked( + cache, "group", "tag", 200, value, NULL, 0, force, &was_changed); + } + + void release() + { + if (out.value.type == NEU_TYPE_PTR) { + free(out.value.value.ptr.ptr); + } else { + neu_free_dvalue(&out.value); + } + out = {}; + for (int i = 0; i < n_meta; i++) { + if (metas[i].value.type == NEU_TYPE_PTR) { + free(metas[i].value.value.ptr.ptr); + } else { + neu_free_dvalue(&metas[i].value); + } + } + free(metas); + metas = NULL; + n_meta = 0; + } + + int read(bool only_changed = false) + { + release(); + return only_changed ? neu_driver_cache_meta_get_changed( + cache, "group", "tag", &out, &metas, &n_meta) + : neu_driver_cache_meta_get(cache, "group", "tag", + &out, &metas, &n_meta); + } + + int type_get() + { + return neu_driver_cache_type_get(cache, "group", "tag", &type, + &generation); + } + + neu_dvalue_t error() + { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_ERROR; + value.value.i32 = 123; + return value; + } + + neu_dvalue_t dynamic(neu_type_e type, const char *text = "value") + { + neu_dvalue_t value = {}; + value.type = type; + if (type == NEU_TYPE_PTR) { + value.value.ptr.type = NEU_TYPE_STRING; + value.value.ptr.length = strlen(text) + 1; + value.value.ptr.ptr = + reinterpret_cast(const_cast(text)); + } else if (type == NEU_TYPE_ARRAY_STRING) { + value.value.strs.length = 1; + value.value.strs.strs = + static_cast(calloc(1, sizeof(char *))); + value.value.strs.strs[0] = strdup(text); + } else if (type == NEU_TYPE_CUSTOM) { + value.value.json = json_pack("{s:s}", "key", text); + } else { + value.value.i32s.length = 1; + value.value.i32s.i32s = + static_cast(calloc(1, sizeof(int32_t))); + value.value.i32s.i32s[0] = text[0]; + } + return value; + } + + bool was_changed = false; + neu_type_e type = NEU_TYPE_AUTO; + uint64_t generation = 0; +}; + +TEST_F(DriverCacheAuto, copy_dynamic_dvalue_preserves_attrs_and_ownership) +{ + for (neu_type_e kind : { NEU_TYPE_PTR, NEU_TYPE_ARRAY_INT32, + NEU_TYPE_ARRAY_STRING, NEU_TYPE_CUSTOM }) { + SCOPED_TRACE(kind); + neu_dvalue_t src = dynamic(kind); + src.attrs = sample().attrs; + neu_dvalue_t dst = {}; + neu_copy_dvalue(&src, &dst); + EXPECT_EQ(src.type, dst.type); + expect_attrs(src.attrs, dst.attrs); + if (kind == NEU_TYPE_PTR) { + EXPECT_EQ(src.value.ptr.ptr, dst.value.ptr.ptr); + EXPECT_EQ(src.value.ptr.length, dst.value.ptr.length); + EXPECT_EQ(src.value.ptr.type, dst.value.ptr.type); + } else if (kind == NEU_TYPE_ARRAY_INT32) { + EXPECT_NE(src.value.i32s.i32s, dst.value.i32s.i32s); + EXPECT_EQ(1u, dst.value.i32s.length); + EXPECT_EQ('v', dst.value.i32s.i32s[0]); + dst.value.i32s.i32s[0] = 'x'; + EXPECT_EQ('v', src.value.i32s.i32s[0]); + } else if (kind == NEU_TYPE_ARRAY_STRING) { + EXPECT_NE(src.value.strs.strs, dst.value.strs.strs); + EXPECT_NE(src.value.strs.strs[0], dst.value.strs.strs[0]); + EXPECT_EQ(1u, dst.value.strs.length); + EXPECT_STREQ("value", dst.value.strs.strs[0]); + dst.value.strs.strs[0][0] = 'x'; + EXPECT_STREQ("value", src.value.strs.strs[0]); + } else { + EXPECT_NE(src.value.json, dst.value.json); + EXPECT_TRUE(json_equal(src.value.json, dst.value.json)); + json_object_set_new(dst.value.json, "key", json_string("other")); + EXPECT_STREQ( + "value", + json_string_value(json_object_get(src.value.json, "key"))); + } + neu_free_dvalue(&src); + neu_free_dvalue(&dst); + } +} + +TEST_F(DriverCacheAuto, dynamic_cache_attrs_and_filtered_last_good_are_owned) +{ + sub_filter_err = true; + for (neu_type_e kind : { NEU_TYPE_PTR, NEU_TYPE_ARRAY_INT32, + NEU_TYPE_ARRAY_STRING, NEU_TYPE_CUSTOM }) { + SCOPED_TRACE(kind); + configure(); + char text[] = "value"; + neu_dvalue_t value = dynamic(kind, text); + value.attrs = sample().attrs; + neu_tag_sample_attrs_t expected = value.attrs; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + text[0] = 'x'; + ASSERT_EQ(0, read(true)); + expect_attrs(expected, out.value.attrs); + if (kind == NEU_TYPE_PTR) { + EXPECT_NE(value.value.ptr.ptr, out.value.value.ptr.ptr); + EXPECT_EQ('v', out.value.value.ptr.ptr[0]); + out.value.value.ptr.ptr[0] = 'z'; + } + out.value.attrs.quality = 0; + out.value.attrs.source_timestamp = 42; + out.value.attrs.server_timestamp = 43; + ASSERT_EQ(0, read()); + expect_attrs(expected, out.value.attrs); + if (kind == NEU_TYPE_PTR) { + EXPECT_EQ('v', out.value.value.ptr.ptr[0]); + } + + ASSERT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + ASSERT_EQ(0, read()); + expect_attrs(error().attrs, out.value.attrs); + + value = dynamic(kind); + value.attrs = expected; + value.attrs.timestamp++; + value.attrs.has_source_timestamp = false; + value.attrs.server_timestamp = 0; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + ASSERT_EQ(0, read()); + expected = value.attrs; + expected.source_timestamp = 0; + expect_attrs(expected, out.value.attrs); + + ASSERT_EQ(NEU_ERR_SUCCESS, checked(error())); + value = dynamic(kind); + value.attrs = expected; + value.attrs.quality = 0; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + expected.quality = 0; + expect_attrs(expected, out.value.attrs); + } +} + +TEST_F(DriverCacheAuto, unresolved_error_then_first_success) +{ + EXPECT_EQ(0, NEU_TYPE_AUTO); + EXPECT_STREQ("NEU_TYPE_AUTO", neu_type_string(NEU_TYPE_AUTO)); + ASSERT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + EXPECT_EQ(NEU_TYPE_AUTO, type); + uint64_t registered = generation; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + EXPECT_EQ(registered, generation); + ASSERT_EQ(NEU_ERR_SUCCESS, checked(sample())); + EXPECT_TRUE(was_changed); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_DOUBLE, type); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(type, out.resolved_type); + EXPECT_EQ(registered, out.generation); + EXPECT_EQ(NEU_TYPE_DOUBLE, out.value.type); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(sample())); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); +} + +TEST_F(DriverCacheAuto, mismatch_publishes_error_and_original_type_recovers) +{ + ASSERT_EQ(NEU_ERR_SUCCESS, checked(sample())); + ASSERT_EQ(0, read(true)); + uint64_t registered = out.generation; + neu_dvalue_t wrong = {}; + wrong.type = NEU_TYPE_INT16; + wrong.value.i16 = 42; + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, checked(wrong)); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, out.value.value.i32); + EXPECT_EQ(NEU_TYPE_DOUBLE, out.resolved_type); + EXPECT_EQ(registered, out.generation); + EXPECT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_DOUBLE, type); + ASSERT_EQ(NEU_ERR_SUCCESS, checked(error())); + ASSERT_EQ(0, read()); + EXPECT_EQ(NEU_TYPE_DOUBLE, out.resolved_type); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(sample())); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(1.25, out.value.value.d64); + EXPECT_EQ(registered, out.generation); +} + +TEST_F(DriverCacheAuto, generation_changes_on_rebuild_not_resolution_or_rename) +{ + ASSERT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + uint64_t first = generation; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(sample())); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(first, generation); + neu_driver_cache_rename(cache, "group", "tag", "renamed"); + EXPECT_EQ(NEU_ERR_SUCCESS, + neu_driver_cache_type_get(cache, "group", "renamed", &type, + &generation)); + EXPECT_EQ(first, generation); + neu_driver_cache_del(cache, "group", "renamed"); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, + neu_driver_cache_type_get(cache, "group", "renamed", &type, + &generation)); + configure(); + ASSERT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + EXPECT_GT(generation, first); + uint64_t second = generation; + configure(); + ASSERT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + EXPECT_GT(generation, second); + neu_dvalue_t value = {}; + value.type = NEU_TYPE_BOOL; + value.value.boolean = true; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_BOOL, type); +} + +TEST_F(DriverCacheAuto, explicit_type_resolves_before_success) +{ + configure(NEU_TYPE_INT16); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_INT16, type); + ASSERT_EQ(0, read()); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + EXPECT_EQ(NEU_TYPE_INT16, out.resolved_type); + EXPECT_EQ(generation, out.generation); +} + +TEST_F(DriverCacheAuto, rejects_nonsemantic_and_unsupported_samples) +{ + for (neu_type_e invalid : + { NEU_TYPE_AUTO, static_cast(35), NEU_TYPE_PTR }) { + neu_dvalue_t value = {}; + value.type = invalid; + if (invalid == NEU_TYPE_PTR) { + value.value.ptr.type = NEU_TYPE_ERROR; + } + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, checked(value)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + ASSERT_EQ(0, read()); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + EXPECT_EQ(NEU_TYPE_AUTO, out.resolved_type); + } + for (neu_type_e invalid : { NEU_TYPE_AUTO, NEU_TYPE_ERROR, NEU_TYPE_PTR, + static_cast(35) }) { + configure(invalid); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + } +} + +TEST_F(DriverCacheAuto, first_sample_validates_deferred_numeric_configuration) +{ + neu_dvalue_t string = {}; + string.type = NEU_TYPE_STRING; + strcpy(string.value.str, "hello"); + for (int option = 0; option < 3; option++) { + configure(NEU_TYPE_AUTO, option == 2 ? 2 : 0, option == 0 ? 0.1 : 0, + option == 1 ? 1 : 0); + int expected = option == 0 + ? NEU_ERR_TAG_DECIMAL_INVALID + : option == 1 ? NEU_ERR_TAG_BIAS_INVALID + : NEU_ERR_TAG_PRECISION_INVALID; + EXPECT_EQ(expected, checked(string)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + ASSERT_EQ(0, read()); + EXPECT_EQ(expected, out.value.value.i32); + ASSERT_EQ(NEU_ERR_SUCCESS, checked(sample())); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_DOUBLE, type); + } +} + +TEST_F(DriverCacheAuto, configured_precision_used_for_current_and_last_good) +{ + for (neu_type_e numeric : { NEU_TYPE_FLOAT, NEU_TYPE_DOUBLE }) { + for (bool filtered : { false, true }) { + configure(NEU_TYPE_AUTO, 2); + sub_filter_err = filtered; + neu_dvalue_t value = {}; + value.type = numeric; + if (numeric == NEU_TYPE_FLOAT) { + value.value.f32 = 1.0f; + } else { + value.value.d64 = 1.0; + } + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(2, out.value.precision); + if (filtered) { + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_FALSE(was_changed); + } + if (numeric == NEU_TYPE_FLOAT) { + value.value.f32 = 1.005f; + } else { + value.value.d64 = 1.005; + } + value.precision = 7; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + if (filtered) { + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + } + if (numeric == NEU_TYPE_FLOAT) { + value.value.f32 = 1.1f; + } else { + value.value.d64 = 1.1; + } + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(2, out.value.precision); + } + } +} + +TEST_F(DriverCacheAuto, attrs_and_force_use_existing_change_semantics) +{ + neu_dvalue_t value = sample(); + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + ASSERT_EQ(0, read(true)); + value.attrs.timestamp++; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + value.attrs.quality++; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(value.attrs.quality, out.value.attrs.quality); + value.attrs.has_timestamp = false; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value, true)); + EXPECT_FALSE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_FALSE(out.value.attrs.has_timestamp); + EXPECT_EQ(0, out.value.attrs.timestamp); + EXPECT_EQ(NEU_TYPE_DOUBLE, out.resolved_type); +} + +TEST_F(DriverCacheAuto, ptr_is_copied_and_resolves_string_or_bytes) +{ + for (neu_type_e semantic : { NEU_TYPE_STRING, NEU_TYPE_BYTES }) { + configure(); + char text[] = "hello"; + neu_dvalue_t value = dynamic(NEU_TYPE_PTR, text); + value.value.ptr.type = semantic; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + text[0] = 'x'; + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(semantic, type); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(NEU_TYPE_PTR, out.value.type); + EXPECT_EQ(semantic, out.value.value.ptr.type); + EXPECT_EQ('h', out.value.value.ptr.ptr[0]); + EXPECT_EQ(semantic, out.resolved_type); + value.value.ptr.type = + semantic == NEU_TYPE_STRING ? NEU_TYPE_BYTES : NEU_TYPE_STRING; + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, checked(value)); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(semantic, type); + } +} + +TEST_F(DriverCacheAuto, dynamic_error_recovery_and_filtered_last_good_lifetimes) +{ + for (neu_type_e dynamic_type : { NEU_TYPE_PTR, NEU_TYPE_ARRAY_INT32, + NEU_TYPE_ARRAY_STRING, NEU_TYPE_CUSTOM }) { + for (bool filtered : { false, true }) { + configure(); + sub_filter_err = filtered; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(dynamic(dynamic_type))); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_EQ(!filtered, was_changed); + if (filtered) { + EXPECT_NE(0, read(true)); + } else { + ASSERT_EQ(0, read(true)); + EXPECT_EQ(NEU_TYPE_ERROR, out.value.type); + } + EXPECT_EQ(NEU_ERR_SUCCESS, checked(dynamic(dynamic_type))); + EXPECT_EQ(!filtered, was_changed); + if (filtered) { + EXPECT_NE(0, read(true)); + } else { + ASSERT_EQ(0, read(true)); + } + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(dynamic(dynamic_type, "other"))); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(dynamic_type, out.value.type); + configure(); + } + } +} + +TEST_F(DriverCacheAuto, rejected_dynamic_samples_are_disposed) +{ + sub_filter_err = true; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(sample())); + ASSERT_EQ(0, read(true)); + for (neu_type_e dynamic_type : + { NEU_TYPE_ARRAY_INT32, NEU_TYPE_ARRAY_STRING, NEU_TYPE_CUSTOM }) { + neu_dvalue_t value = dynamic(dynamic_type); + json_t * json = dynamic_type == NEU_TYPE_CUSTOM + ? json_incref(value.value.json) + : NULL; + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, checked(value)); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + if (json) { + EXPECT_EQ(1u, json->refcount); + json_decref(json); + } + EXPECT_EQ(NEU_ERR_SUCCESS, checked(sample())); + EXPECT_FALSE(was_changed); + } + neu_driver_cache_del(cache, "group", "tag"); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, + checked(dynamic(NEU_TYPE_ARRAY_STRING))); +} + +TEST_F(DriverCacheAuto, filtered_first_zero_success_is_a_change) +{ + sub_filter_err = true; + neu_dvalue_t zero = {}; + zero.type = NEU_TYPE_INT32; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(zero)); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(NEU_TYPE_INT32, out.resolved_type); +} + +TEST_F(DriverCacheAuto, snapshots_pair_value_with_registration_and_type) +{ + std::atomic done(false); + std::thread writer([&] { + for (int i = 0; i < 2000; i++) { + configure(); + neu_dvalue_t value = {}; + value.type = i % 2 ? NEU_TYPE_DOUBLE : NEU_TYPE_INT32; + neu_driver_cache_update(cache, "group", "tag", i, value, NULL, 0); + } + done = true; + }); + uint64_t previous = 0; + do { + int ret = read(); + EXPECT_EQ(0, ret); + if (ret == 0) { + EXPECT_GE(out.generation, previous); + previous = out.generation; + if (out.value.type != NEU_TYPE_ERROR) { + EXPECT_EQ(out.value.type, out.resolved_type); + } + } + } while (!done); + writer.join(); +} + +TEST_F(DriverCacheAuto, nonfinite_samples_do_not_resolve_or_clear_type) +{ + for (neu_type_e numeric : { NEU_TYPE_FLOAT, NEU_TYPE_DOUBLE }) { + configure(); + for (double invalid : { std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity() }) { + neu_dvalue_t value = {}; + value.type = numeric; + if (numeric == NEU_TYPE_FLOAT) { + value.value.f32 = invalid; + } else { + value.value.d64 = invalid; + } + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, checked(value)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + ASSERT_EQ(0, read()); + EXPECT_EQ(NEU_TYPE_AUTO, out.resolved_type); + } + neu_dvalue_t valid = {}; + valid.type = numeric; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(valid)); + EXPECT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(numeric, type); + } +} + +TEST_F(DriverCacheAuto, initial_success_is_validated_and_owned) +{ + neu_datatag_t tag = {}; + tag.name = const_cast("tag"); + tag.type = NEU_TYPE_AUTO; + tag.precision = 2; + neu_driver_cache_add_tag(cache, "group", &tag, dynamic(NEU_TYPE_CUSTOM)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, type_get()); + ASSERT_EQ(0, read()); + EXPECT_EQ(NEU_ERR_TAG_PRECISION_INVALID, out.value.value.i32); + sub_filter_err = true; + neu_driver_cache_add_tag(cache, "group", &tag, sample()); + ASSERT_EQ(NEU_ERR_SUCCESS, type_get()); + EXPECT_EQ(NEU_TYPE_DOUBLE, type); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(2, out.value.precision); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(sample())); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); +} + +TEST_F(DriverCacheAuto, every_numeric_array_owns_current_last_good_and_snapshot) +{ + sub_filter_err = true; + for (int kind = NEU_TYPE_ARRAY_INT8; kind <= NEU_TYPE_ARRAY_BOOL; kind++) { + configure(); + neu_dvalue_t empty = {}; + empty.type = static_cast(kind); + ASSERT_EQ(NEU_ERR_SUCCESS, checked(empty)); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(empty.type, out.resolved_type); + EXPECT_EQ(0u, out.value.value.bools.length); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(empty)); + EXPECT_FALSE(was_changed); + auto array = [&] { + neu_dvalue_t value = {}; + value.type = empty.type; + value.value.bools.length = 2; + value.value.bools.bools = + static_cast(calloc(2, sizeof(double))); + return value; + }; + ASSERT_EQ(NEU_ERR_SUCCESS, checked(array())); + EXPECT_TRUE(was_changed); + ASSERT_EQ(0, read(true)); + EXPECT_EQ(2u, out.value.value.bools.length); + out.value.value.bools.bools[0] = true; + ASSERT_EQ(0, read()); + EXPECT_FALSE(out.value.value.bools.bools[0]); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(error())); + EXPECT_EQ(NEU_ERR_SUCCESS, checked(array())); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + } +} + +TEST_F(DriverCacheAuto, metadata_snapshots_copy_ptr_and_preserve_attrs) +{ + neu_tag_meta_t input[2] = {}; + strcpy(input[0].name, "text"); + char text[] = "hello"; + input[0].value = dynamic(NEU_TYPE_PTR, text); + input[0].value.attrs.has_quality = true; + input[0].value.attrs.quality = 192; + input[1].value = dynamic(NEU_TYPE_ARRAY_STRING); + ASSERT_EQ(NEU_ERR_SUCCESS, + neu_driver_cache_update_checked(cache, "group", "tag", 200, + sample(), input, 2, false, + &was_changed)); + text[0] = 'x'; + ASSERT_EQ(0, read(true)); + ASSERT_EQ(2, n_meta); + EXPECT_STREQ("text", metas[0].name); + EXPECT_EQ('h', metas[0].value.value.ptr.ptr[0]); + EXPECT_EQ(192, metas[0].value.attrs.quality); + EXPECT_EQ(NEU_TYPE_INT8, metas[1].value.type); + metas[0].value.value.ptr.ptr[0] = 'z'; + ASSERT_EQ(0, read()); + EXPECT_EQ('h', metas[0].value.value.ptr.ptr[0]); + input[0].value = dynamic(NEU_TYPE_PTR, "replacement"); + ASSERT_EQ(NEU_ERR_SUCCESS, + neu_driver_cache_update_checked(cache, "group", "tag", 201, + sample(), input, 1, false, + &was_changed)); + ASSERT_EQ(0, read()); + EXPECT_EQ(1, n_meta); + EXPECT_EQ('r', metas[0].value.value.ptr.ptr[0]); +} + +TEST_F(DriverCacheAuto, generation_is_local_to_cache_instance) +{ + neu_driver_cache_t *other = neu_driver_cache_new(); + neu_datatag_t tag = {}; + tag.name = const_cast("tag"); + tag.type = NEU_TYPE_BOOL; + neu_driver_cache_add_tag(other, "group", &tag, error()); + ASSERT_EQ( + NEU_ERR_SUCCESS, + neu_driver_cache_type_get(other, "group", "tag", &type, &generation)); + EXPECT_EQ(1u, generation); + neu_driver_cache_destroy(other); +} + +TEST_F(DriverCacheAuto, scalar_comparisons_ignore_unused_union_bytes) +{ + for (neu_type_e scalar : + { NEU_TYPE_INT8, NEU_TYPE_UINT8, NEU_TYPE_INT16, NEU_TYPE_UINT16, + NEU_TYPE_INT32, NEU_TYPE_UINT32, NEU_TYPE_INT64, NEU_TYPE_UINT64, + NEU_TYPE_WORD, NEU_TYPE_DWORD, NEU_TYPE_LWORD, NEU_TYPE_BIT, + NEU_TYPE_BOOL, NEU_TYPE_STRING, NEU_TYPE_TIME, + NEU_TYPE_DATA_AND_TIME }) { + for (bool filtered : { false, true }) { + configure(); + sub_filter_err = filtered; + neu_dvalue_t value = {}; + value.type = scalar; + memset(&value.value, 0xa5, sizeof(value.value)); + if (scalar == NEU_TYPE_STRING || scalar == NEU_TYPE_TIME || + scalar == NEU_TYPE_DATA_AND_TIME) { + strcpy(value.value.str, "same"); + } else { + value.value.u64 = 0; + } + ASSERT_EQ(NEU_ERR_SUCCESS, checked(value)); + ASSERT_EQ(0, read(true)); + if (filtered) { + ASSERT_EQ(NEU_ERR_SUCCESS, checked(error())); + } + memset(&value.value, 0, sizeof(value.value)); + if (scalar == NEU_TYPE_STRING || scalar == NEU_TYPE_TIME || + scalar == NEU_TYPE_DATA_AND_TIME) { + strcpy(value.value.str, "same"); + } + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + EXPECT_FALSE(was_changed); + EXPECT_NE(0, read(true)); + } + } +} + +TEST_F(DriverCacheAuto, explicit_mapped_types_keep_legacy_sample_acceptance) +{ + for (neu_type_e alias : { NEU_TYPE_WORD, NEU_TYPE_DWORD, NEU_TYPE_LWORD }) { + configure(alias); + neu_dvalue_t value = {}; + value.type = alias == NEU_TYPE_WORD + ? NEU_TYPE_UINT16 + : alias == NEU_TYPE_DWORD ? NEU_TYPE_UINT32 : NEU_TYPE_UINT64; + value.value.u64 = 42; + EXPECT_EQ(NEU_ERR_SUCCESS, checked(value)); + ASSERT_EQ(0, read()); + EXPECT_EQ(value.type, out.value.type); + EXPECT_EQ(alias, out.resolved_type); + } +} diff --git a/tests/ut/driver_auto_helpers.c b/tests/ut/driver_auto_helpers.c new file mode 100644 index 000000000..0deb77291 --- /dev/null +++ b/tests/ut/driver_auto_helpers.c @@ -0,0 +1,147 @@ +#include "../../src/adapter/driver/driver.c" + +bool sub_filter_err = false; +zlog_category_t *neuron = NULL; + +static void capture_timestamp(void *arg, int64_t timestamp, UT_array *tags, + uint32_t interval) +{ + (void) interval; + ((group_t *) arg)->timestamp = timestamp; + utarray_free(tags); +} + +int auto_test_prepare(neu_type_e resolved, neu_dvalue_t *value, double decimal, + double bias, const char *address, int invalidate, + bool *valid, neu_type_e *written_type) +{ + neu_adapter_driver_t driver = { 0 }; + group_t group = { .name = "g" }; + neu_datatag_t tag = { .name = "a", + .address = (char *) address, + .description = "", + .unit = "", + .type = NEU_TYPE_AUTO, + .attribute = NEU_ATTRIBUTE_READ | NEU_ATTRIBUTE_WRITE, + .decimal = decimal, + .bias = bias }; + neu_dvalue_t initial = { .type = NEU_TYPE_ERROR, + .value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY }; + driver.cache = neu_driver_cache_new(); + HASH_ADD_STR(driver.groups, name, &group); + group.group = neu_group_new("g", 100); + neu_group_add_tag(group.group, &tag); + neu_group_change_test(group.group, 0, &group, capture_timestamp); + neu_driver_cache_add_tag(driver.cache, "g", &tag, initial); + if (resolved != NEU_TYPE_AUTO) { + neu_dvalue_t sample = { .type = resolved }; + neu_driver_cache_update(driver.cache, "g", "a", 100, sample, NULL, 0); + } + UT_array *types = NULL; + int ret = prepare_write_tag(&driver, &group, &tag, value, &types); + *written_type = tag.type; + if (invalidate == 1) { + tag.type = NEU_TYPE_AUTO; + tag.address = "changed"; + neu_group_update_tag(group.group, &tag); + } else if (invalidate == 2) { + tag.type = NEU_TYPE_AUTO; + neu_driver_cache_add_tag(driver.cache, "g", &tag, initial); + } else if (invalidate == 3) { + neu_group_del_tag(group.group, "a"); + } + *valid = write_types_valid(&driver, types); + if (types != NULL) { + utarray_free(types); + } + HASH_DEL(driver.groups, &group); + neu_group_destroy(group.group); + neu_driver_cache_destroy(driver.cache); + return ret; +} + +int auto_test_metadata(neu_type_e resolved, bool pending, int *count, + neu_type_e *type) +{ + neu_adapter_driver_t driver = { 0 }; + group_t group = { .name = "g" }; + neu_datatag_t tag = { .name = "a", + .address = "sample", + .description = "", + .unit = "", + .type = NEU_TYPE_AUTO, + .attribute = + NEU_ATTRIBUTE_READ | NEU_ATTRIBUTE_WRITE }; + driver.cache = neu_driver_cache_new(); + group.group = neu_group_new("g", 100); + HASH_ADD_STR(driver.groups, name, &group); + neu_group_add_tag(group.group, &tag); + neu_group_change_test(group.group, 0, &group, capture_timestamp); + neu_dvalue_t initial = { .type = NEU_TYPE_ERROR, + .value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY }; + neu_driver_cache_add_tag(driver.cache, "g", &tag, initial); + if (resolved != NEU_TYPE_AUTO) { + neu_dvalue_t sample = { .type = resolved }; + neu_driver_cache_update(driver.cache, "g", "a", 100, sample, NULL, 0); + } + if (pending) { + group.timestamp = 0; + } + UT_array *tags = NULL; + int ret = neu_adapter_driver_get_value_tag(&driver, "g", &tags); + *count = tags ? utarray_len(tags) : 0; + *type = tags && utarray_len(tags) + ? ((neu_datatag_t *) utarray_front(tags))->type + : NEU_TYPE_AUTO; + if (tags) { + utarray_free(tags); + } + HASH_DEL(driver.groups, &group); + neu_group_destroy(group.group); + neu_driver_cache_destroy(driver.cache); + return ret; +} + +void auto_test_read(neu_dvalue_t sample, double decimal, const char *address, + int path, neu_dvalue_t *output, neu_type_e *config_type) +{ + neu_driver_cache_t *cache = neu_driver_cache_new(); + neu_datatag_t tag = { .name = "a", + .address = (char *) address, + .description = "", + .unit = "", + .type = NEU_TYPE_AUTO, + .attribute = NEU_ATTRIBUTE_READ, + .decimal = decimal }; + neu_dvalue_t initial = { .type = NEU_TYPE_ERROR, + .value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY }; + neu_driver_cache_add_tag(cache, "g", &tag, initial); + neu_driver_cache_update(cache, "g", "a", 100, sample, NULL, 0); + UT_array *tags, *values; + utarray_new(tags, neu_tag_get_icd()); + utarray_push_back(tags, &tag); + if (path == 2) { + utarray_new(values, neu_resp_tag_value_meta_paginate_icd()); + read_group_paginate(100, 1000, NEU_TAG_CACHE_TYPE_NEVER, cache, "g", + tags, values); + neu_resp_tag_value_meta_paginate_t *v = utarray_front(values); + *output = v->value; + *config_type = v->datatag.type; + neu_tag_fini(&v->datatag); + } else { + utarray_new(values, neu_resp_tag_value_meta_icd()); + if (path == 0) { + read_group(100, 1000, NEU_TAG_CACHE_TYPE_NEVER, cache, "g", tags, + values); + } else { + read_report_group(100, 1000, NEU_TAG_CACHE_TYPE_NEVER, cache, "g", + tags, values); + } + neu_resp_tag_value_meta_t *v = utarray_front(values); + *output = v->value; + *config_type = v->datatag.type; + } + utarray_free(values); + utarray_free(tags); + neu_driver_cache_destroy(cache); +} diff --git a/tests/ut/driver_auto_test.cc b/tests/ut/driver_auto_test.cc new file mode 100644 index 000000000..4b0a69142 --- /dev/null +++ b/tests/ut/driver_auto_test.cc @@ -0,0 +1,301 @@ +#include +#include + +extern "C" { +#include "errcodes.h" +#include "type.h" +int is_value_in_range(neu_type_e, int64_t, double, neu_type_e, double); +int auto_test_prepare(neu_type_e, neu_dvalue_t *, double, double, const char *, + int, bool *, neu_type_e *); +void auto_test_read(neu_dvalue_t, double, const char *, int, neu_dvalue_t *, + neu_type_e *); +int auto_test_metadata(neu_type_e, bool, int *, neu_type_e *); +} + +TEST(DriverAuto, UnresolvedWrite) +{ + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = 123; + bool valid; + neu_type_e type; + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, + auto_test_prepare(NEU_TYPE_AUTO, &value, 0, 0, "sample", 0, + &valid, &type)); +} + +TEST(DriverAuto, ConcreteWriteAndEndian) +{ + for (const char *address : { "sample", "sample#B" }) { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = 123; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_INT16, &value, 0, 0, address, 0, + &valid, &type)); + EXPECT_EQ(NEU_TYPE_INT16, type); + EXPECT_EQ(NEU_TYPE_INT16, value.type); + EXPECT_EQ(address[6] ? htons(123) : 123, value.value.u16); + EXPECT_TRUE(valid); + } +} + +TEST(DriverAuto, RangeAndRealInputType) +{ + for (neu_type_e input : { NEU_TYPE_INT64, NEU_TYPE_STRING }) { + neu_dvalue_t value = {}; + value.type = input; + value.value.i64 = 40000; + bool valid; + neu_type_e type; + EXPECT_EQ(input == NEU_TYPE_INT64 + ? NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE + : NEU_ERR_PLUGIN_TAG_TYPE_MISMATCH, + auto_test_prepare(NEU_TYPE_INT16, &value, 0, 0, "sample", 0, + &valid, &type)); + } +} + +TEST(DriverAuto, PendingConfigAndGenerationInvalidation) +{ + for (int invalidate : { 1, 2, 3 }) { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = 123; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_INT16, &value, 0, 0, "sample", + invalidate, &valid, &type)); + EXPECT_FALSE(valid); + } +} + +TEST(DriverAuto, ScaledSignedWrite) +{ + neu_dvalue_t value = {}; + value.type = NEU_TYPE_DOUBLE; + value.value.d64 = -123.5; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_INT16, &value, 0.5, 0, "sample", 0, + &valid, &type)); + EXPECT_EQ(-247, value.value.i16); + EXPECT_TRUE(valid); +} + +TEST(DriverAuto, BiasAndScaleWriteInverse) +{ + for (double decimal : { 0.0, 0.5 }) { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = 51; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_INT16, &value, decimal, 1, + "sample", 0, &valid, &type)); + EXPECT_EQ(decimal == 0 ? 50 : 100, value.value.i16); + EXPECT_TRUE(valid); + } +} + +TEST(DriverAuto, WideIntegerBiasPreservesPrecision) +{ + for (int64_t input : { INT64_C(9007199254740994), INT64_MAX }) { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = input; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_INT64, &value, 0, 1, "sample", 0, + &valid, &type)); + EXPECT_EQ(input - 1, value.value.i64); + EXPECT_TRUE(valid); + } +} + +TEST(DriverAuto, FloatBiasPreservesIntegerPrecision) +{ + for (neu_type_e target : { NEU_TYPE_FLOAT, NEU_TYPE_DOUBLE }) { + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = INT64_C(9007199254740993); + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(target, &value, 0, 9007199254740992.0, + "sample", 0, &valid, &type)); + if (target == NEU_TYPE_FLOAT) { + EXPECT_FLOAT_EQ(1, value.value.f32); + } else { + EXPECT_DOUBLE_EQ(1, value.value.d64); + } + EXPECT_TRUE(valid); + } +} + +TEST(DriverAuto, ValueMetadataMustBeConcreteOrNotReady) +{ + int count; + neu_type_e type; + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, + auto_test_metadata(NEU_TYPE_AUTO, false, &count, &type)); + EXPECT_EQ(0, count); + EXPECT_EQ(0, auto_test_metadata(NEU_TYPE_INT16, false, &count, &type)); + EXPECT_EQ(1, count); + EXPECT_EQ(NEU_TYPE_INT16, type); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_NOT_READY, + auto_test_metadata(NEU_TYPE_INT16, true, &count, &type)); + EXPECT_EQ(0, count); +} + +TEST(DriverAuto, FloatWriteUsesNumericUnion) +{ + neu_dvalue_t value = {}; + value.type = NEU_TYPE_INT64; + value.value.i64 = 123; + bool valid; + neu_type_e type; + EXPECT_EQ(0, + auto_test_prepare(NEU_TYPE_FLOAT, &value, 0.5, 0, "sample", 0, + &valid, &type)); + EXPECT_FLOAT_EQ(246, value.value.f32); +} + +TEST(DriverAuto, NonfiniteAndWideBounds) +{ + for (double value : { std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN() }) { + EXPECT_EQ( + NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, + is_value_in_range(NEU_TYPE_FLOAT, 0, value, NEU_TYPE_DOUBLE, 0)); + EXPECT_EQ( + NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, + is_value_in_range(NEU_TYPE_INT64, 0, value, NEU_TYPE_DOUBLE, 0.5)); + } + EXPECT_EQ( + NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, + is_value_in_range(NEU_TYPE_INT64, INT64_MAX, 0, NEU_TYPE_INT64, 0.5)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, + is_value_in_range(NEU_TYPE_UINT64, -1, 0, NEU_TYPE_INT64, 0)); + EXPECT_EQ(0, + is_value_in_range(NEU_TYPE_FLOAT, 0, 1e30, NEU_TYPE_DOUBLE, 0)); + EXPECT_EQ(NEU_ERR_PLUGIN_TAG_VALUE_OUT_OF_RANGE, + is_value_in_range(NEU_TYPE_FLOAT, 0, 1e40, NEU_TYPE_DOUBLE, 0)); +} + +static neu_tag_sample_attrs_t sample_attrs() +{ + neu_tag_sample_attrs_t attrs = {}; + attrs.has_timestamp = true; + attrs.timestamp = INT64_C(1700000000000); + attrs.has_source_timestamp = true; + attrs.source_timestamp = INT64_C(1700000000123); + attrs.has_server_timestamp = true; + attrs.server_timestamp = INT64_C(1700000000456); + attrs.has_quality = true; + attrs.quality = 192; + return attrs; +} + +static void expect_attrs(const neu_tag_sample_attrs_t &expected, + const neu_tag_sample_attrs_t &actual) +{ + EXPECT_EQ(expected.has_timestamp, actual.has_timestamp); + EXPECT_EQ(expected.has_source_timestamp, actual.has_source_timestamp); + EXPECT_EQ(expected.has_server_timestamp, actual.has_server_timestamp); + EXPECT_EQ(expected.has_quality, actual.has_quality); + EXPECT_EQ(expected.has_timestamp ? expected.timestamp : 0, + actual.timestamp); + EXPECT_EQ(expected.has_source_timestamp ? expected.source_timestamp : 0, + actual.source_timestamp); + EXPECT_EQ(expected.has_server_timestamp ? expected.server_timestamp : 0, + actual.server_timestamp); + EXPECT_EQ(expected.has_quality ? expected.quality : 0, actual.quality); +} + +TEST(DriverAuto, ReadPathsScaleWithoutChangingConfig) +{ + for (int path : { 0, 1, 2 }) { + neu_dvalue_t sample = {}; + sample.type = NEU_TYPE_INT16; + sample.value.i16 = -246; + sample.attrs = sample_attrs(); + neu_dvalue_t output = {}; + neu_type_e config; + auto_test_read(sample, 0.5, "sample", path, &output, &config); + EXPECT_EQ(NEU_TYPE_AUTO, config); + EXPECT_EQ(NEU_TYPE_DOUBLE, output.type); + EXPECT_DOUBLE_EQ(-123, output.value.d64); + expect_attrs(sample.attrs, output.attrs); + } +} + +TEST(DriverAuto, ReadPathsResolveEndian) +{ + for (int path : { 0, 1, 2 }) { + neu_dvalue_t sample = {}; + sample.type = NEU_TYPE_INT16; + sample.value.u16 = htons(123); + sample.attrs = sample_attrs(); + neu_dvalue_t output = {}; + neu_type_e config; + auto_test_read(sample, 0, "sample#B", path, &output, &config); + EXPECT_EQ(NEU_TYPE_AUTO, config); + EXPECT_EQ(NEU_TYPE_INT16, output.type); + EXPECT_EQ(123, output.value.i16); + expect_attrs(sample.attrs, output.attrs); + } +} + +TEST(DriverAuto, ReadPathsPreserveOptionalTimestamps) +{ + for (int path : { 0, 1, 2 }) { + for (neu_type_e type : + { NEU_TYPE_INT16, NEU_TYPE_PTR, NEU_TYPE_ERROR }) { + for (int flags = 0; flags < 8; ++flags) { + SCOPED_TRACE(::testing::Message() + << "path=" << path << " type=" << type + << " flags=" << flags); + char text[] = "hello"; + neu_dvalue_t sample = {}; + sample.type = type; + sample.attrs = sample_attrs(); + sample.attrs.has_timestamp = flags & 1; + sample.attrs.has_source_timestamp = flags & 2; + sample.attrs.has_server_timestamp = flags & 4; + sample.attrs.source_timestamp = 0; + if (type == NEU_TYPE_PTR) { + sample.value.ptr.type = NEU_TYPE_STRING; + sample.value.ptr.length = sizeof(text); + sample.value.ptr.ptr = (uint8_t *) text; + } else if (type == NEU_TYPE_ERROR) { + sample.value.i32 = NEU_ERR_PLUGIN_TAG_NOT_READY; + } else { + sample.value.i16 = 123; + } + neu_dvalue_t output = {}; + neu_type_e config; + auto_test_read(sample, 0, "sample", path, &output, &config); + EXPECT_EQ(NEU_TYPE_AUTO, config); + EXPECT_EQ(type, output.type); + expect_attrs(sample.attrs, output.attrs); + if (output.type == NEU_TYPE_PTR) { + EXPECT_NE(sample.value.ptr.ptr, output.value.ptr.ptr); + EXPECT_STREQ(text, (char *) output.value.ptr.ptr); + free(output.value.ptr.ptr); + } else if (output.type == NEU_TYPE_ERROR) { + EXPECT_EQ(sample.value.i32, output.value.i32); + } else { + EXPECT_EQ(123, output.value.i16); + } + } + } + } +} diff --git a/tests/ut/json_test.cc b/tests/ut/json_test.cc index 1d640b92c..90a9a5a19 100644 --- a/tests/ut/json_test.cc +++ b/tests/ut/json_test.cc @@ -1,5 +1,10 @@ +#include #include +#include +#include +#include +#include "parser/neu_json_node.h" #include "utils/log.h" #include "json/json.h" @@ -328,6 +333,309 @@ TEST(JsonTest, EncodeArray) neu_json_decode_free(ob); } +namespace { + +using PropertyJson = std::unique_ptr; + +const char *property_values[] = { + R"("text")", + R"("")", + "true", + "false", + "0", + "-1", + "-9223372036854775808", + "9223372036854775807", + "9007199254740993", + "1.25", + "0.0", + "-0.0", + "1.7976931348623157e308", + "2.2250738585072014e-308", + "5e-324", + R"(["", "中文", "a\"b", "a\u0000b"])", + "[true, false]", + "[-9223372036854775808, 0, 9223372036854775807]", + "[0.0, -1.5, 2.5]", + "[0, 1.5, -2, 0.0]", + "[]", + R"("中文\"\\\n\r\t\b\f\u0000end")", +}; + +void expect_property_json(const char *expected_text, const char *actual_text) +{ + PropertyJson expected( + json_loads(expected_text, JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL), + json_decref); + PropertyJson actual( + json_loads(actual_text, JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL), + json_decref); + ASSERT_NE(nullptr, expected.get()); + ASSERT_NE(nullptr, actual.get()); + EXPECT_EQ(json_typeof(expected.get()), json_typeof(actual.get())); + EXPECT_TRUE(json_equal(expected.get(), actual.get())); + if (json_is_real(expected.get()) && json_real_value(expected.get()) == 0) { + EXPECT_EQ(std::signbit(json_real_value(expected.get())), + std::signbit(json_real_value(actual.get()))); + } +} + +void expect_property_response(neu_node_property_t *properties, size_t count) +{ + neu_json_get_node_properties_resp_t response = { count, properties }; + char * encoded = NULL; + ASSERT_EQ(0, neu_json_encode_get_node_properties_resp(&response, &encoded)); + std::unique_ptr buffer(encoded, free); + ASSERT_NE(nullptr, buffer.get()); + PropertyJson decoded(json_loads(buffer.get(), JSON_ALLOW_NUL, NULL), + json_decref); + ASSERT_NE(nullptr, decoded.get()); + EXPECT_EQ(1, json_object_size(decoded.get())); + json_t *array = json_object_get(decoded.get(), "properties"); + ASSERT_TRUE(json_is_array(array)); + ASSERT_EQ(count, json_array_size(array)); + for (size_t i = 0; i < count; ++i) { + json_t *item = json_array_get(array, i); + EXPECT_EQ(2, json_object_size(item)); + ASSERT_TRUE(json_is_string(json_object_get(item, "key"))); + EXPECT_STREQ(properties[i].key, + json_string_value(json_object_get(item, "key"))); + char *value = + neu_node_property_json_dump(json_object_get(item, "value")); + ASSERT_NE(nullptr, value); + expect_property_json(properties[i].value_json, value); + free(value); + } +} + +} // namespace + +TEST(JsonTest, NodePropertiesAddAndResponsePreserveTypes) +{ + const size_t count = sizeof(property_values) / sizeof(property_values[0]); + std::string input = R"({"node":"driver","properties":[)"; + for (size_t i = 0; i < count; ++i) { + if (i != 0) { + input += ","; + } + input += "{\"key\":\"key" + std::to_string(i) + + "\",\"value\":" + property_values[i] + "}"; + } + input += "]}"; + neu_json_add_node_properties_req_t *raw = NULL; + ASSERT_EQ(0, + neu_json_decode_add_node_properties_req( + const_cast(input.c_str()), &raw)); + std::unique_ptr + request(raw, neu_json_decode_add_node_properties_req_free); + ASSERT_NE(nullptr, request.get()); + EXPECT_STREQ("driver", request->node); + ASSERT_EQ(count, request->n_property); + for (size_t i = 0; i < request->n_property; ++i) { + SCOPED_TRACE(property_values[i]); + EXPECT_EQ("key" + std::to_string(i), request->properties[i].key); + expect_property_json(property_values[i], + request->properties[i].value_json); + } + expect_property_response(request->properties, request->n_property); +} + +TEST(JsonTest, NodePropertyUpdateAndResponsePreserveTypes) +{ + for (const char *value : property_values) { + SCOPED_TRACE(value); + std::string input = R"({"node":"app","key":"转义\"\\\n","value":)"; + input += value; + input += "}"; + neu_json_update_node_property_req_t *raw = NULL; + ASSERT_EQ(0, + neu_json_decode_update_node_property_req( + const_cast(input.c_str()), &raw)); + std::unique_ptr + request(raw, neu_json_decode_update_node_property_req_free); + ASSERT_NE(nullptr, request.get()); + EXPECT_STREQ("app", request->node); + EXPECT_STREQ("转义\"\\\n", request->property.key); + expect_property_json(value, request->property.value_json); + expect_property_response(&request->property, 1); + } +} + +TEST(JsonTest, NodePropertiesEmptyResponse) +{ + expect_property_response(NULL, 0); +} + +TEST(JsonTest, NodePropertiesDelete) +{ + char input[] = R"({"node":"app","keys":["missing","转义\"\\\n"]})"; + neu_json_delete_node_properties_req_t *raw = NULL; + ASSERT_EQ(0, neu_json_decode_delete_node_properties_req(input, &raw)); + std::unique_ptr + request(raw, neu_json_decode_delete_node_properties_req_free); + ASSERT_NE(nullptr, request.get()); + EXPECT_STREQ("app", request->node); + ASSERT_EQ(2, request->n_key); + EXPECT_STREQ("missing", request->keys[0]); + EXPECT_STREQ("转义\"\\\n", request->keys[1]); +} + +TEST(JsonTest, NodePropertiesRejectInvalidValues) +{ + const char *invalid[] = { + "null", + "{}", + R"({"a":1})", + "[[1]]", + "[[]]", + "[null]", + "[{}]", + R"([1,"1"])", + "[true,1]", + "[false,0.0]", + R"(["true",true])", + "9223372036854775808", + "-9223372036854775809", + "1e309", + "NaN", + "Infinity", + }; + for (const char *value : invalid) { + SCOPED_TRACE(value); + std::string add = + R"({"node":"driver","properties":[{"key":"valid","value":0},{"key":"bad","value":)"; + add += value; + add += "}]}"; + neu_json_add_node_properties_req_t *add_req = NULL; + EXPECT_NE(0, + neu_json_decode_add_node_properties_req( + const_cast(add.c_str()), &add_req)); + EXPECT_EQ(nullptr, add_req); + neu_json_decode_add_node_properties_req_free(add_req); + + std::string update = R"({"node":"app","key":"bad","value":)"; + update += value; + update += "}"; + neu_json_update_node_property_req_t *update_req = NULL; + EXPECT_NE(0, + neu_json_decode_update_node_property_req( + const_cast(update.c_str()), &update_req)); + EXPECT_EQ(nullptr, update_req); + neu_json_decode_update_node_property_req_free(update_req); + } +} + +TEST(JsonTest, NodePropertiesRejectInvalidAdd) +{ + const char *invalid[] = { + "", + "{", + "[]", + "null", + "{}", + R"({"properties":[{"key":"a","value":0}]})", + R"({"node":"","properties":[{"key":"a","value":0}]})", + R"({"node":1,"properties":[{"key":"a","value":0}]})", + R"({"node":null,"properties":[{"key":"a","value":0}]})", + R"({"node":"a\u0000b","properties":[{"key":"a","value":0}]})", + R"({"node":"driver"})", + R"({"node":"driver","properties":[]})", + R"({"node":"driver","properties":{}})", + R"({"node":"driver","properties":null})", + R"({"node":"driver","properties":[null]})", + R"({"node":"driver","properties":[{"value":0}]})", + R"({"node":"driver","properties":[{"key":"" ,"value":0}]})", + R"({"node":"driver","properties":[{"key":false,"value":0}]})", + R"({"node":"driver","properties":[{"key":"a\u0000b","value":0}]})", + R"({"node":"driver","properties":[{"key":"a"}]})", + R"({"node":"driver","properties":[{"key":"a","value":0},{"key":"a","value":1}]})", + R"({"node":"driver","properties":[{"key":"a","key":"b","value":0}]})", + R"({"node":"driver","properties":[{"key":"a","value":0,"value":1}]})", + R"({"node":"driver","node":"app","properties":[{"key":"a","value":0}]})", + }; + for (const char *input : invalid) { + SCOPED_TRACE(input); + neu_json_add_node_properties_req_t *request = NULL; + EXPECT_NE(0, + neu_json_decode_add_node_properties_req( + const_cast(input), &request)); + EXPECT_EQ(nullptr, request); + neu_json_decode_add_node_properties_req_free(request); + } +} + +TEST(JsonTest, NodePropertiesRejectInvalidDelete) +{ + const char *invalid[] = { + "", + "{", + "[]", + "null", + "{}", + R"({"keys":["a"]})", + R"({"node":"","keys":["a"]})", + R"({"node":null,"keys":["a"]})", + R"({"node":1,"keys":["a"]})", + R"({"node":"a\u0000b","keys":["a"]})", + R"({"node":"app"})", + R"({"node":"app","keys":[]})", + R"({"node":"app","keys":null})", + R"({"node":"app","keys":"a"})", + R"({"node":"app","keys":["valid",""]})", + R"({"node":"app","keys":["valid",null]})", + R"({"node":"app","keys":["valid",1]})", + R"({"node":"app","keys":["valid",[]]})", + R"({"node":"app","keys":["valid",{}]})", + R"({"node":"app","keys":["valid","a\u0000b"]})", + R"({"node":"app","keys":["a","a"]})", + R"({"node":"app","keys":["a"],"keys":["b"]})", + }; + for (const char *input : invalid) { + SCOPED_TRACE(input); + neu_json_delete_node_properties_req_t *request = NULL; + EXPECT_NE(0, + neu_json_decode_delete_node_properties_req( + const_cast(input), &request)); + EXPECT_EQ(nullptr, request); + neu_json_decode_delete_node_properties_req_free(request); + } +} + +TEST(JsonTest, NodePropertiesRejectInvalidUpdate) +{ + const char *invalid[] = { + "", + "{", + "[]", + "null", + "{}", + R"({"key":"a","value":0})", + R"({"node":"","key":"a","value":0})", + R"({"node":null,"key":"a","value":0})", + R"({"node":1,"key":"a","value":0})", + R"({"node":"a\u0000b","key":"a","value":0})", + R"({"node":"app","value":0})", + R"({"node":"app","key":"a"})", + R"({"node":"app","key":"","value":0})", + R"({"node":"app","key":false,"value":0})", + R"({"node":"app","key":"a\u0000b","value":0})", + R"({"node":"app","key":"a","value":0,"value":1})", + }; + for (const char *input : invalid) { + SCOPED_TRACE(input); + neu_json_update_node_property_req_t *request = NULL; + EXPECT_NE(0, + neu_json_decode_update_node_property_req( + const_cast(input), &request)); + EXPECT_EQ(nullptr, request); + neu_json_decode_update_node_property_req_free(request); + } +} + int main(int argc, char **argv) { zlog_init("./config/dev.conf"); diff --git a/tests/ut/node_property_persist_test.cc b/tests/ut/node_property_persist_test.cc new file mode 100644 index 000000000..5418c7a2a --- /dev/null +++ b/tests/ut/node_property_persist_test.cc @@ -0,0 +1,642 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "errcodes.h" +#include "persist/persist.h" +#include "persist/sqlite.h" +#include "utils/log.h" + +zlog_category_t *neuron = nullptr; + +namespace { + +neu_node_property_t property(const char *key, const char *value) +{ + return { const_cast(key), const_cast(value) }; +} + +struct Properties { + neu_node_property_t *items = nullptr; + size_t n = 0; + + ~Properties() { neu_node_properties_free(items, n); } +}; + +class NodePropertyPersist : public testing::Test { +protected: + int cwd = -1; + std::string directory; + bool opened = false; + sqlite3 * db = nullptr; + + void SetUp() override + { + cwd = open(".", O_RDONLY | O_DIRECTORY); + ASSERT_GE(cwd, 0); + char path[] = "/tmp/neuron-property-XXXXXX"; + ASSERT_NE(nullptr, mkdtemp(path)); + directory = path; + ASSERT_EQ(0, mkdir((directory + "/persistence").c_str(), 0700)); + ASSERT_EQ(0, mkdir((directory + "/schemas").c_str(), 0700)); + std::filesystem::copy_file( + std::string(NEU_PROPERTY_SCHEMA_DIR) + + "/0160_2.16.1_node_properties.sql", + directory + "/schemas/0160_2.16.1_node_properties.sql"); + ASSERT_EQ(0, chdir(directory.c_str())); + ASSERT_EQ(SQLITE_OK, + sqlite3_open_v2("persistence/sqlite.db", &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, + nullptr)); + ASSERT_EQ(SQLITE_OK, + sqlite3_exec( + db, + "CREATE TABLE nodes(name TEXT PRIMARY KEY, type INTEGER, " + "state INTEGER, plugin_name TEXT, tags TEXT);" + "CREATE TABLE settings(node_name TEXT PRIMARY KEY, " + "setting TEXT, " + "FOREIGN KEY(node_name) REFERENCES nodes(name) ON UPDATE " + "CASCADE ON DELETE CASCADE);" + "CREATE TABLE groups(driver_name TEXT, name TEXT, " + "interval INTEGER, " + "context TEXT, PRIMARY KEY(driver_name,name), " + "FOREIGN KEY(driver_name) REFERENCES nodes(name) ON " + "UPDATE CASCADE ON DELETE CASCADE);" + "CREATE TABLE tags(driver_name TEXT, group_name TEXT, " + "name TEXT, " + "address TEXT, attribute INTEGER, precision INTEGER, " + "type INTEGER, " + "decimal REAL, bias REAL, description TEXT, value TEXT, " + "format TEXT, unit TEXT, " + "UNIQUE(driver_name,group_name,name), FOREIGN " + "KEY(driver_name,group_name) " + "REFERENCES groups(driver_name,name) ON UPDATE CASCADE " + "ON DELETE CASCADE);", + nullptr, nullptr, nullptr)) + << sqlite3_errmsg(db); + ASSERT_EQ(SQLITE_OK, sqlite3_close(db)); + db = nullptr; + ASSERT_EQ(0, neu_persister_create("schemas")); + opened = true; + db = neu_persister_get_db(); + ASSERT_NE(nullptr, sqlite3_db_mutex(db)); + neu_persist_node_info_t driver = { const_cast("driver"), 1, + const_cast("modbus"), 1, + nullptr }; + neu_persist_node_info_t app = { const_cast("app"), 2, + const_cast("mqtt"), 1, + nullptr }; + ASSERT_EQ(0, neu_persister_store_node(&driver)); + ASSERT_EQ(0, neu_persister_store_node(&app)); + } + + void TearDown() override + { + if (opened) { + neu_persister_destroy(); + } else if (db) { + sqlite3_close(db); + } + if (cwd >= 0) { + EXPECT_EQ(0, fchdir(cwd)); + close(cwd); + } + if (!directory.empty()) { + std::filesystem::remove_all(directory); + } + } + + void sql(const char *query) + { + ASSERT_EQ(SQLITE_OK, sqlite3_exec(db, query, nullptr, nullptr, nullptr)) + << sqlite3_errmsg(db); + } + + int scalar(const char *query) + { + sqlite3_stmt *stmt = nullptr; + if (SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, nullptr)) { + ADD_FAILURE() << sqlite3_errmsg(db); + return -1; + } + int step = sqlite3_step(stmt); + EXPECT_EQ(SQLITE_ROW, step); + int value = step == SQLITE_ROW ? sqlite3_column_int(stmt, 0) : -1; + EXPECT_EQ(SQLITE_OK, sqlite3_finalize(stmt)); + return value; + } + + void expect(const char * node, + const std::vector> &values) + { + Properties loaded; + ASSERT_EQ( + 0, + neu_persister_load_node_properties(node, &loaded.items, &loaded.n)); + ASSERT_EQ(values.size(), loaded.n); + if (values.empty()) { + EXPECT_EQ(nullptr, loaded.items); + } + for (size_t i = 0; i < loaded.n; ++i) { + EXPECT_EQ(values[i].first, loaded.items[i].key); + EXPECT_EQ(values[i].second, loaded.items[i].value_json); + } + } + + void group() + { + neu_persist_group_info_t info = { 1000, const_cast("group"), + nullptr }; + ASSERT_EQ(0, neu_persister_store_group("driver", &info, nullptr)); + } + + neu_datatag_t tag(const char *name) + { + neu_datatag_t value = {}; + value.name = const_cast(name); + value.address = const_cast("1!400001"); + value.description = const_cast(""); + value.unit = const_cast(""); + value.type = NEU_TYPE_INT16; + value.attribute = NEU_ATTRIBUTE_READ; + value.decimal = 1; + return value; + } + + void concurrent_rollback(bool tags); +}; + +TEST_F(NodePropertyPersist, CrudAndNodeIsolation) +{ + expect("driver", {}); + neu_node_property_t values[] = { property("z", "true"), + property("a", "1") }; + ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2)); + auto app = property("a", "\"north\""); + ASSERT_EQ(0, neu_persister_add_node_properties("app", &app, 1)); + expect("driver", { { "a", "1" }, { "z", "true" } }); + expect("app", { { "a", "\"north\"" } }); + + auto update = property("a", "[1,2.5]"); + ASSERT_EQ(0, neu_persister_update_node_property("driver", &update)); + ASSERT_EQ(0, neu_persister_update_node_property("driver", &update)); + auto missing = property("missing", "false"); + EXPECT_EQ(NEU_ERR_NODE_PROPERTY_NOT_EXIST, + neu_persister_update_node_property("driver", &missing)); + expect("driver", { { "a", "[1,2.5]" }, { "z", "true" } }); + + const char *keys[] = { "a", "missing" }; + ASSERT_EQ(0, neu_persister_delete_node_properties("driver", keys, 2)); + ASSERT_EQ(0, neu_persister_delete_node_properties("driver", keys, 2)); + expect("driver", { { "z", "true" } }); + expect("app", { { "a", "\"north\"" } }); + const char *last[] = { "z" }; + ASSERT_EQ(0, neu_persister_delete_node_properties("driver", last, 1)); + expect("driver", {}); +} + +TEST_F(NodePropertyPersist, ValueTypesAndCanonicalRoundTrip) +{ + const char * texts[] = { " \"text\\n\\\"中文\" ", + "\"\\u0000\"", + "-9223372036854775808", + "9223372036854775807", + " 1.25e2 ", + "1.7976931348623157e308", + "2.2250738585072014e-308", + "5e-324", + "[1.7976931348623157e308,5e-324]", + "true", + "false", + "[]", + "[\"a\",\"b\",\"\\u0000\"]", + "[1, -2, 3.5]", + "[1.0, 2]", + "[true,false]" }; + std::vector keys; + std::vector values; + for (size_t i = 0; i < std::size(texts); ++i) { + keys.push_back("key" + std::to_string(100 + i)); + } + for (size_t i = 0; i < keys.size(); ++i) { + values.push_back(property(keys[i].c_str(), texts[i])); + } + ASSERT_EQ(0, + neu_persister_add_node_properties("driver", values.data(), + values.size())); + Properties loaded; + ASSERT_EQ( + 0, + neu_persister_load_node_properties("driver", &loaded.items, &loaded.n)); + ASSERT_EQ(values.size(), loaded.n); + for (size_t i = 0; i < loaded.n; ++i) { + json_t *expected = + json_loads(texts[i], JSON_DECODE_ANY | JSON_ALLOW_NUL, nullptr); + json_t *actual = json_loads(loaded.items[i].value_json, + JSON_DECODE_ANY | JSON_ALLOW_NUL, nullptr); + ASSERT_NE(nullptr, expected); + ASSERT_NE(nullptr, actual); + EXPECT_TRUE(json_equal(expected, actual)); + char *canonical = neu_node_property_json_dump(expected); + EXPECT_STREQ(canonical, loaded.items[i].value_json); + free(canonical); + json_decref(expected); + json_decref(actual); + } +} + +TEST_F(NodePropertyPersist, ExistingAndBatchDuplicatesRollback) +{ + auto seed = property("existing", "1"); + ASSERT_EQ(0, neu_persister_add_node_properties("driver", &seed, 1)); + neu_node_property_t existing[] = { property("new", "2"), + property("existing", "3") }; + EXPECT_EQ(NEU_ERR_NODE_PROPERTY_EXIST, + neu_persister_add_node_properties("driver", existing, 2)); + neu_node_property_t duplicate[] = { property("new", "2"), + property("new", "2") }; + EXPECT_EQ(NEU_ERR_NODE_PROPERTY_EXIST, + neu_persister_add_node_properties("driver", duplicate, 2)); + expect("driver", { { "existing", "1" } }); + EXPECT_TRUE(sqlite3_get_autocommit(db)); +} + +TEST_F(NodePropertyPersist, DeleteDuplicateAndInvalidKeysRollback) +{ + neu_node_property_t values[] = { property("a", "1"), property("b", "2") }; + ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2)); + const char *duplicate[] = { "a", "missing", "a" }; + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", duplicate, 3)); + const char *empty[] = { "a", "" }; + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", empty, 2)); + const char *null_key[] = { "a", nullptr }; + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", null_key, 2)); + const char *missing_duplicate[] = { "missing", "missing" }; + EXPECT_EQ( + NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", missing_duplicate, 2)); + expect("driver", { { "a", "1" }, { "b", "2" } }); +} + +TEST_F(NodePropertyPersist, RejectInvalidParameters) +{ + auto value = property("key", "1"); + const char *keys[] = { "key" }; + for (const char *node : { static_cast(nullptr), "" }) { + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_add_node_properties(node, &value, 1)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties(node, keys, 1)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_update_node_property(node, &value)); + Properties loaded; + EXPECT_EQ( + NEU_ERR_PARAM_IS_WRONG, + neu_persister_load_node_properties(node, &loaded.items, &loaded.n)); + EXPECT_EQ(nullptr, loaded.items); + EXPECT_EQ(0u, loaded.n); + } + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_add_node_properties("driver", nullptr, 1)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_add_node_properties("driver", &value, 0)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", nullptr, 1)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_delete_node_properties("driver", keys, 0)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_update_node_property("driver", nullptr)); + for (const char *key : { static_cast(nullptr), "" }) { + auto invalid = property(key, "1"); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_add_node_properties("driver", &invalid, 1)); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_update_node_property("driver", &invalid)); + } + size_t n = 10; + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_load_node_properties("driver", nullptr, &n)); + EXPECT_EQ(0u, n); + auto *items = reinterpret_cast(1); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_load_node_properties("driver", &items, nullptr)); + EXPECT_EQ(nullptr, items); + EXPECT_EQ( + NEU_ERR_PARAM_IS_WRONG, + neu_sqlite_persister_add_node_properties(nullptr, "driver", &value, 1)); + expect("driver", {}); +} + +TEST_F(NodePropertyPersist, RejectInvalidValuesAndRollback) +{ + const char *invalid[] = { + nullptr, "", "null", "{}", "[{}]", + "[[1]]", "[1,true]", "[false,0.5]", "[\"x\",1]", "[null]", + "[1,]", "1 2", "NaN", "Infinity", "\"unfinished" + }; + for (const char *text : invalid) { + neu_node_property_t values[] = { property("first", "1"), + property("invalid", text) }; + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_add_node_properties("driver", values, 2)) + << (text ? text : "null pointer"); + expect("driver", {}); + } + auto value = property("key", "1"); + ASSERT_EQ(0, neu_persister_add_node_properties("driver", &value, 1)); + for (const char *text : invalid) { + auto update = property("key", text); + EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG, + neu_persister_update_node_property("driver", &update)); + } + expect("driver", { { "key", "1" } }); +} + +TEST_F(NodePropertyPersist, BoundTextAndCascades) +{ + neu_persist_node_info_t node = { const_cast("node'--"), 2, + const_cast("mqtt"), 1, nullptr }; + ASSERT_EQ(0, neu_persister_store_node(&node)); + const char *key = "中文'; DROP TABLE nodes;--"; + auto value = property(key, "\"quote' and \\\"json\\\"\""); + ASSERT_EQ(0, neu_persister_add_node_properties(node.name, &value, 1)); + expect(node.name, { { key, value.value_json } }); + ASSERT_EQ(0, neu_persister_update_node(node.name, "renamed")); + expect(node.name, {}); + expect("renamed", { { key, value.value_json } }); + ASSERT_EQ(0, neu_persister_delete_node("renamed")); + expect("renamed", {}); + EXPECT_EQ(2, scalar("SELECT count(*) FROM nodes")); + EXPECT_EQ(0, scalar("SELECT count(*) FROM node_properties")); + EXPECT_NE(0, neu_persister_add_node_properties("nonexistent", &value, 1)); + EXPECT_TRUE(sqlite3_get_autocommit(db)); +} + +TEST_F(NodePropertyPersist, PersistenceReopenAndMigration) +{ + auto value = property("key", "[true,false]"); + ASSERT_EQ(0, neu_persister_add_node_properties("driver", &value, 1)); + neu_persister_destroy(); + opened = false; + db = nullptr; + ASSERT_EQ(0, neu_persister_create("schemas")); + opened = true; + db = neu_persister_get_db(); + expect("driver", { { "key", "[true,false]" } }); + EXPECT_EQ(1, + scalar("SELECT count(*) FROM migrations WHERE version='0160' AND " + "dirty=0")); + EXPECT_EQ(1, scalar("PRAGMA foreign_keys")); + EXPECT_NE(nullptr, sqlite3_db_mutex(db)); +} + +TEST_F(NodePropertyPersist, FailedLoadClearsOutputs) +{ + neu_node_property_t values[] = { property("a", "1"), property("b", "2") }; + ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2)); + sql("UPDATE node_properties SET value_json='[1,true]' WHERE key='b'"); + auto * items = reinterpret_cast(1); + size_t n = 42; + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_load_node_properties("driver", &items, &n)); + EXPECT_EQ(nullptr, items); + EXPECT_EQ(0u, n); + sql("DROP TABLE node_properties"); + items = reinterpret_cast(1); + n = 42; + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_load_node_properties("driver", &items, &n)); + EXPECT_EQ(nullptr, items); + EXPECT_EQ(0u, n); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_add_node_properties("driver", values, 2)); + EXPECT_TRUE(sqlite3_get_autocommit(db)); +} + +TEST_F(NodePropertyPersist, ExistingTransactionIsNeverRolledBack) +{ + group(); + sql("BEGIN"); + sql("UPDATE nodes SET state=2 WHERE name='driver'"); + auto value = property("key", "1"); + const char *keys[] = { "key" }; + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_add_node_properties("driver", &value, 1)); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_delete_node_properties("driver", keys, 1)); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_update_node_property("driver", &value)); + Properties loaded; + EXPECT_EQ( + NEU_ERR_EINTERNAL, + neu_persister_load_node_properties("driver", &loaded.items, &loaded.n)); + auto value_tag = tag("tag"); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_store_tags("driver", "group", &value_tag, 1)); + EXPECT_FALSE(sqlite3_get_autocommit(db)); + EXPECT_EQ(2, scalar("SELECT state FROM nodes WHERE name='driver'")); + sql("ROLLBACK"); + EXPECT_EQ(1, scalar("SELECT state FROM nodes WHERE name='driver'")); +} + +TEST_F(NodePropertyPersist, StepAndBindErrorsRollback) +{ + neu_node_property_t values[] = { property("a", "1"), property("b", "2") }; + ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2)); + sql("CREATE TRIGGER fail_delete BEFORE DELETE ON node_properties " + "WHEN OLD.key='b' BEGIN SELECT RAISE(ABORT, 'fail'); END"); + const char *keys[] = { "a", "b" }; + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_delete_node_properties("driver", keys, 2)); + expect("driver", { { "a", "1" }, { "b", "2" } }); + std::string large = "\"" + std::string(2048, 'x') + "\""; + auto value = property("large", large.c_str()); + int old = sqlite3_limit(db, SQLITE_LIMIT_LENGTH, 1024); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_add_node_properties("driver", &value, 1)); + sqlite3_limit(db, SQLITE_LIMIT_LENGTH, old); + expect("driver", { { "a", "1" }, { "b", "2" } }); + EXPECT_TRUE(sqlite3_get_autocommit(db)); +} + +int deny_commit(void *, int action, const char *arg, const char *, const char *, + const char *) +{ + return action == SQLITE_TRANSACTION && arg && strcmp(arg, "COMMIT") == 0 + ? SQLITE_DENY + : SQLITE_OK; +} + +TEST_F(NodePropertyPersist, CommitErrorsRollback) +{ + group(); + auto seed = property("a", "1"); + ASSERT_EQ(0, neu_persister_add_node_properties("driver", &seed, 1)); + ASSERT_EQ(SQLITE_OK, sqlite3_set_authorizer(db, deny_commit, nullptr)); + auto added = property("b", "2"); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_add_node_properties("driver", &added, 1)); + auto updated = property("a", "3"); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_update_node_property("driver", &updated)); + const char *keys[] = { "a" }; + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_delete_node_properties("driver", keys, 1)); + auto value_tag = tag("tag"); + EXPECT_EQ(NEU_ERR_EINTERNAL, + neu_persister_store_tags("driver", "group", &value_tag, 1)); + EXPECT_TRUE(sqlite3_get_autocommit(db)); + ASSERT_EQ(SQLITE_OK, sqlite3_set_authorizer(db, nullptr, nullptr)); + expect("driver", { { "a", "1" } }); + EXPECT_EQ(0, scalar("SELECT count(*) FROM tags")); +} + +struct TransactionGate { + std::mutex mutex; + std::condition_variable changed; + bool paused = false; + bool released = false; + std::atomic started { 0 }; + std::atomic ended { false }; + std::atomic interleaved { false }; + std::thread::id owner; + std::string insert; +}; + +int transaction_trace(unsigned, void *context, void *statement, void *) +{ + auto & gate = *static_cast(context); + const char *query = sqlite3_sql(static_cast(statement)); + if (std::this_thread::get_id() != gate.owner) { + if (!gate.ended.load()) { + gate.interleaved = true; + } + return 0; + } + if (strcmp(query, "ROLLBACK") == 0) { + gate.ended = true; + } + if (strncmp(query, gate.insert.c_str(), gate.insert.size()) == 0) { + std::unique_lock lock(gate.mutex); + if (!gate.paused) { + gate.paused = true; + gate.changed.notify_all(); + gate.changed.wait(lock, [&] { return gate.released; }); + } + } + return 0; +} + +void NodePropertyPersist::concurrent_rollback(bool tags) +{ + group(); + TransactionGate gate; + gate.insert = tags ? "INSERT INTO tags" : "INSERT INTO node_properties"; + ASSERT_EQ( + SQLITE_OK, + sqlite3_trace_v2(db, SQLITE_TRACE_STMT, transaction_trace, &gate)); + std::vector names; + for (int i = 0; i < 500; ++i) { + names.push_back("batch" + std::to_string(i)); + } + std::vector properties; + std::vector datatags; + for (const auto &name : names) { + properties.push_back(property(name.c_str(), "1")); + datatags.push_back(tag(name.c_str())); + } + properties.push_back(properties[0]); + datatags.push_back(datatags[0]); + int batch_result = -1; + int other_result = -1; + int ordinary_result = -1; + std::thread batch([&] { + gate.owner = std::this_thread::get_id(); + batch_result = tags + ? neu_persister_store_tags("driver", "group", datatags.data(), + datatags.size()) + : neu_persister_add_node_properties("driver", properties.data(), + properties.size()); + }); + bool paused; + { + std::unique_lock lock(gate.mutex); + paused = gate.changed.wait_for(lock, std::chrono::seconds(5), + [&] { return gate.paused; }); + } + std::thread other([&] { + ++gate.started; + if (tags) { + auto value = property("saved", "true"); + other_result = neu_persister_add_node_properties("app", &value, 1); + } else { + auto value = tag("saved"); + other_result = + neu_persister_store_tags("driver", "group", &value, 1); + } + }); + std::thread ordinary([&] { + ++gate.started; + ordinary_result = neu_persister_store_node_setting("driver", "saved"); + }); + while (gate.started.load() != 2) { + std::this_thread::yield(); + } + { + std::lock_guard lock(gate.mutex); + gate.released = true; + } + gate.changed.notify_all(); + batch.join(); + other.join(); + ordinary.join(); + EXPECT_EQ(SQLITE_OK, sqlite3_trace_v2(db, 0, nullptr, nullptr)); + ASSERT_TRUE(paused); + EXPECT_TRUE(gate.ended.load()); + EXPECT_FALSE(gate.interleaved.load()); + EXPECT_EQ(tags ? NEU_ERR_EINTERNAL : NEU_ERR_NODE_PROPERTY_EXIST, + batch_result); + EXPECT_EQ(0, other_result); + EXPECT_EQ(0, ordinary_result); + EXPECT_TRUE(sqlite3_get_autocommit(db)); + expect("driver", {}); + expect("app", + tags + ? std::vector> { { "saved", + "true" } } + : std::vector> {}); + EXPECT_EQ(tags ? 0 : 1, scalar("SELECT count(*) FROM tags")); + const char *setting = nullptr; + ASSERT_EQ(0, neu_persister_load_node_setting("driver", &setting)); + EXPECT_STREQ("saved", setting); + free(const_cast(setting)); +} + +TEST_F(NodePropertyPersist, PropertyTransactionIsolatesTagsAndOrdinaryWrite) +{ + concurrent_rollback(false); +} + +TEST_F(NodePropertyPersist, TagTransactionIsolatesPropertiesAndOrdinaryWrite) +{ + concurrent_rollback(true); +} + +}