This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-core.git
commit 7797b37814ed57baf6153852f5cb80413ee9b480 Author: Szymon Janc <[email protected]> AuthorDate: Wed Oct 14 23:55:36 2026 +0200 net/mqtt/coremqtt: Add unit tests for socket transport Tests run coreMQTT over Mynewt socket transport against minimal MQTT broker listening on loopback interface. They cover connection failure, subscribe and QoS 1 publish round trip, publish spanning multiple mbufs and socket reads, ping, receive timeout and broker closing the connection. Assisted-by: Claude:claude-opus-5-5 --- net/mqtt/coremqtt/selftest/pkg.yml | 33 +++ net/mqtt/coremqtt/selftest/src/coremqtt_test.c | 333 +++++++++++++++++++++++++ net/mqtt/coremqtt/selftest/src/coremqtt_test.h | 62 +++++ net/mqtt/coremqtt/selftest/src/test_broker.c | 265 ++++++++++++++++++++ net/mqtt/coremqtt/selftest/syscfg.yml | 25 ++ 5 files changed, 718 insertions(+) diff --git a/net/mqtt/coremqtt/selftest/pkg.yml b/net/mqtt/coremqtt/selftest/pkg.yml new file mode 100644 index 000000000..62da3ac9a --- /dev/null +++ b/net/mqtt/coremqtt/selftest/pkg.yml @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +pkg.name: net/mqtt/coremqtt/selftest +pkg.type: unittest +pkg.description: "coreMQTT and Mynewt socket transport unit tests." +pkg.author: "Apache Mynewt <[email protected]>" +pkg.homepage: "http://mynewt.apache.org/" +pkg.keywords: + +pkg.deps: + - "@apache-mynewt-core/net/ip/mn_socket" + - "@apache-mynewt-core/net/mqtt/coremqtt" + - "@apache-mynewt-core/net/mqtt/coremqtt/transport/mn_socket" + - "@apache-mynewt-core/sys/console/stub" + - "@apache-mynewt-core/sys/log/stub" + - "@apache-mynewt-core/test/testutil" diff --git a/net/mqtt/coremqtt/selftest/src/coremqtt_test.c b/net/mqtt/coremqtt/selftest/src/coremqtt_test.c new file mode 100644 index 000000000..d4828e880 --- /dev/null +++ b/net/mqtt/coremqtt/selftest/src/coremqtt_test.c @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include <string.h> +#include "os/mynewt.h" +#include "testutil/testutil.h" +#include "mn_socket/mn_socket.h" +#include "core_mqtt.h" +#include "coremqtt/coremqtt.h" +#include "coremqtt/mqtt_mn_socket.h" +#include "coremqtt_test.h" + +#define TEST_TOPIC "mynewt/test" +#define TEST_TIMEOUT_MS 2000 +#define TEST_LARGE_LEN 3000 + +static NetworkContext_t test_net; +static MQTTContext_t test_ctx; +static uint8_t test_buf[TEST_LARGE_LEN + 64]; +static MQTTPubAckInfo_t test_outgoing[2]; +static MQTTPubAckInfo_t test_incoming[2]; +static uint8_t test_payload[TEST_LARGE_LEN]; + +static struct { + int suback; + int puback; + int rx; + uint8_t payload[TEST_LARGE_LEN]; + size_t len; +} test_ev; + +static bool +test_event(MQTTContext_t *ctx, MQTTPacketInfo_t *pkt, + MQTTDeserializedInfo_t *info, MQTTSuccessFailReasonCode_t *reason, + MQTTPropBuilder_t *send_props, MQTTPropBuilder_t *get_props) +{ + MQTTPublishInfo_t *pub; + + switch (pkt->type & 0xF0) { + case MQTT_PACKET_TYPE_SUBACK: + test_ev.suback++; + break; + case MQTT_PACKET_TYPE_PUBACK: + test_ev.puback++; + break; + case MQTT_PACKET_TYPE_PUBLISH: + pub = info->pPublishInfo; + TEST_ASSERT(pub->topicNameLength == strlen(TEST_TOPIC)); + TEST_ASSERT(memcmp(pub->pTopicName, TEST_TOPIC, pub->topicNameLength) == 0); + TEST_ASSERT_FATAL(pub->payloadLength <= sizeof(test_ev.payload)); + memcpy(test_ev.payload, pub->pPayload, pub->payloadLength); + test_ev.len = pub->payloadLength; + test_ev.rx++; + break; + default: + break; + } + + return true; +} + +static int +test_net_connect(uint16_t port) +{ + struct mn_sockaddr_in sin; + + memset(&sin, 0, sizeof(sin)); + sin.msin_len = sizeof(sin); + sin.msin_family = MN_AF_INET; + sin.msin_port = htons(port); + mn_inet_pton(MN_AF_INET, "127.0.0.1", &sin.msin_addr); + + return mqtt_mn_socket_connect(&test_net, (struct mn_sockaddr *)&sin, + TEST_TIMEOUT_MS); +} + +static MQTTStatus_t +test_mqtt_connect(void) +{ + TransportInterface_t transport; + MQTTFixedBuffer_t buf; + MQTTConnectInfo_t conn; + MQTTStatus_t status; + bool session_present; + int rc; + + memset(&test_ev, 0, sizeof(test_ev)); + + rc = test_net_connect(TEST_BROKER_PORT); + TEST_ASSERT_FATAL(rc == 0, "connect rc=%d", rc); + + mqtt_mn_socket_transport(&test_net, &transport); + buf.pBuffer = test_buf; + buf.size = sizeof(test_buf); + + status = MQTT_Init(&test_ctx, &transport, coremqtt_time_ms, test_event, &buf); + TEST_ASSERT_FATAL(status == MQTTSuccess); + status = MQTT_InitStatefulQoS(&test_ctx, test_outgoing, 2, test_incoming, + 2, NULL, 0); + TEST_ASSERT_FATAL(status == MQTTSuccess); + + memset(&conn, 0, sizeof(conn)); + conn.cleanSession = true; + conn.keepAliveSeconds = 60; + conn.pClientIdentifier = "test"; + conn.clientIdentifierLength = 4; + + return MQTT_Connect(&test_ctx, &conn, NULL, TEST_TIMEOUT_MS, + &session_present, NULL, NULL); +} + +static void +test_mqtt_disconnect(void) +{ + MQTTStatus_t status; + + status = MQTT_Disconnect(&test_ctx, NULL, NULL); + TEST_ASSERT(status == MQTTSuccess); + mqtt_mn_socket_disconnect(&test_net); + test_broker_wait_idle(); + TEST_ASSERT(test_broker.err == 0); +} + +static void +test_process_until(int *cnt, int target) +{ + MQTTStatus_t status; + uint32_t start; + + start = coremqtt_time_ms(); + while (*cnt < target) { + status = MQTT_ProcessLoop(&test_ctx); + TEST_ASSERT_FATAL(status == MQTTSuccess || status == MQTTNeedMoreBytes, + "status=%d", status); + TEST_ASSERT_FATAL(coremqtt_time_ms() - start < TEST_TIMEOUT_MS); + } +} + +static void +test_subscribe(void) +{ + MQTTSubscribeInfo_t sub; + MQTTStatus_t status; + + memset(&sub, 0, sizeof(sub)); + sub.qos = MQTTQoS1; + sub.pTopicFilter = TEST_TOPIC; + sub.topicFilterLength = strlen(TEST_TOPIC); + + status = MQTT_Subscribe(&test_ctx, &sub, 1, MQTT_GetPacketId(&test_ctx), NULL); + TEST_ASSERT_FATAL(status == MQTTSuccess); + test_process_until(&test_ev.suback, 1); +} + +static void +test_publish(const void *payload, size_t len) +{ + MQTTPublishInfo_t pub; + MQTTStatus_t status; + + memset(&pub, 0, sizeof(pub)); + pub.qos = MQTTQoS1; + pub.pTopicName = TEST_TOPIC; + pub.topicNameLength = strlen(TEST_TOPIC); + pub.pPayload = payload; + pub.payloadLength = len; + + status = MQTT_Publish(&test_ctx, &pub, MQTT_GetPacketId(&test_ctx), NULL); + TEST_ASSERT_FATAL(status == MQTTSuccess); +} + +static void +coremqtt_test_connect_refused(void) +{ + int rc; + + /* Nothing listens on this port */ + rc = test_net_connect(TEST_BROKER_PORT + 1); + TEST_ASSERT(rc != 0); + TEST_ASSERT(test_net.sock == NULL); +} + +static void +coremqtt_test_pubsub(void) +{ + TEST_ASSERT_FATAL(test_mqtt_connect() == MQTTSuccess); + TEST_ASSERT(test_broker.connects == 1); + + test_subscribe(); + + test_publish("hello", 5); + test_process_until(&test_ev.rx, 1); + test_process_until(&test_ev.puback, 1); + TEST_ASSERT(test_ev.len == 5); + TEST_ASSERT(memcmp(test_ev.payload, "hello", 5) == 0); + TEST_ASSERT(test_broker.publishes == 1); + + test_mqtt_disconnect(); + TEST_ASSERT(test_broker.disconnects == 1); +} + +static void +coremqtt_test_large_publish(void) +{ + size_t i; + + for (i = 0; i < sizeof(test_payload); i++) { + test_payload[i] = i * 7; + } + + TEST_ASSERT_FATAL(test_mqtt_connect() == MQTTSuccess); + test_subscribe(); + + /* Spans several mbufs and several socket reads in both directions */ + test_publish(test_payload, sizeof(test_payload)); + test_process_until(&test_ev.rx, 1); + test_process_until(&test_ev.puback, 1); + TEST_ASSERT(test_ev.len == sizeof(test_payload)); + TEST_ASSERT(memcmp(test_ev.payload, test_payload, sizeof(test_payload)) == 0); + + test_mqtt_disconnect(); +} + +static void +coremqtt_test_ping(void) +{ + MQTTStatus_t status; + uint32_t start; + + TEST_ASSERT_FATAL(test_mqtt_connect() == MQTTSuccess); + + status = MQTT_Ping(&test_ctx); + TEST_ASSERT_FATAL(status == MQTTSuccess); + TEST_ASSERT(test_ctx.waitingForPingResp); + + start = coremqtt_time_ms(); + while (test_ctx.waitingForPingResp) { + status = MQTT_ProcessLoop(&test_ctx); + TEST_ASSERT_FATAL(status == MQTTSuccess); + TEST_ASSERT_FATAL(coremqtt_time_ms() - start < TEST_TIMEOUT_MS); + } + TEST_ASSERT(test_broker.pings == 1); + + test_mqtt_disconnect(); +} + +static void +coremqtt_test_recv_timeout(void) +{ + MQTTStatus_t status; + uint32_t start; + uint32_t elapsed; + + TEST_ASSERT_FATAL(test_mqtt_connect() == MQTTSuccess); + + /* With nothing to receive, the process loop waits for the timeout */ + mqtt_mn_socket_set_recv_timeout(&test_net, 200); + start = coremqtt_time_ms(); + status = MQTT_ProcessLoop(&test_ctx); + elapsed = coremqtt_time_ms() - start; + TEST_ASSERT(status == MQTTSuccess); + TEST_ASSERT(elapsed >= 150 && elapsed < 1000, "elapsed=%u", (unsigned)elapsed); + + /* Without a timeout it returns right away */ + mqtt_mn_socket_set_recv_timeout(&test_net, 0); + start = coremqtt_time_ms(); + status = MQTT_ProcessLoop(&test_ctx); + elapsed = coremqtt_time_ms() - start; + TEST_ASSERT(status == MQTTSuccess); + TEST_ASSERT(elapsed < 100, "elapsed=%u", (unsigned)elapsed); + + test_mqtt_disconnect(); +} + +static void +coremqtt_test_broker_close(void) +{ + MQTTStatus_t status; + + /* Broker drops the connection instead of answering CONNECT */ + test_broker.close_on_connect = 1; + status = test_mqtt_connect(); + test_broker.close_on_connect = 0; + TEST_ASSERT(status != MQTTSuccess); + TEST_ASSERT(test_ctx.connectStatus != MQTTConnected); + + /* Further use of the closed socket fails instead of blocking */ + TEST_ASSERT(mqtt_mn_socket_recv(&test_net, test_buf, 1) < 0); + TEST_ASSERT(mqtt_mn_socket_send(&test_net, "x", 1) < 0); + + mqtt_mn_socket_disconnect(&test_net); + test_broker_wait_idle(); +} + +TEST_CASE_TASK(coremqtt_test_transport) +{ + test_broker_start(); + + coremqtt_test_connect_refused(); + coremqtt_test_pubsub(); + coremqtt_test_large_publish(); + coremqtt_test_ping(); + coremqtt_test_recv_timeout(); + coremqtt_test_broker_close(); +} + +TEST_SUITE(coremqtt_test_all) +{ + coremqtt_test_transport(); +} + +int +main(int argc, char **argv) +{ + coremqtt_test_all(); + return tu_any_failed; +} diff --git a/net/mqtt/coremqtt/selftest/src/coremqtt_test.h b/net/mqtt/coremqtt/selftest/src/coremqtt_test.h new file mode 100644 index 000000000..fb93e5eba --- /dev/null +++ b/net/mqtt/coremqtt/selftest/src/coremqtt_test.h @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef H_COREMQTT_TEST_ +#define H_COREMQTT_TEST_ + +#include "os/mynewt.h" +#include "testutil/testutil.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define TEST_BROKER_PORT 18883 + +/* + * Minimal MQTT v5.0 broker for tests, listening on 127.0.0.1. Accepts one + * client at a time, acknowledges CONNECT, SUBSCRIBE, PUBLISH (QoS 1) and + * PINGREQ, and echoes every PUBLISH back to the client. Runs in the socket + * provider's task. + */ +struct test_broker { + struct mn_socket *listen; + struct mn_socket *conn; + uint8_t rx[4608]; + size_t rx_len; + /* Close the connection instead of sending CONNACK */ + volatile int close_on_connect; + volatile int connects; + volatile int publishes; + volatile int pings; + volatile int disconnects; + /* Non-zero if the broker failed (unexpected input or send error) */ + volatile int err; +}; + +extern struct test_broker test_broker; + +void test_broker_start(void); +void test_broker_wait_idle(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/net/mqtt/coremqtt/selftest/src/test_broker.c b/net/mqtt/coremqtt/selftest/src/test_broker.c new file mode 100644 index 000000000..d5657c936 --- /dev/null +++ b/net/mqtt/coremqtt/selftest/src/test_broker.c @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include <string.h> +#include "os/mynewt.h" +#include "mn_socket/mn_socket.h" +#include "coremqtt_test.h" + +struct test_broker test_broker; + +static void +test_broker_close(void) +{ + if (test_broker.conn) { + mn_close(test_broker.conn); + test_broker.conn = NULL; + } + test_broker.rx_len = 0; +} + +static void +test_broker_send(const uint8_t *data, size_t len) +{ + struct os_mbuf *om; + int rc; + + om = os_msys_get_pkthdr(len, 0); + if (!om) { + test_broker.err = MN_ENOBUFS; + return; + } + if (os_mbuf_append(om, data, len)) { + os_mbuf_free_chain(om); + test_broker.err = MN_ENOBUFS; + return; + } + + rc = mn_sendto(test_broker.conn, om, NULL); + if (rc) { + if (rc == MN_EAGAIN) { + os_mbuf_free_chain(om); + } + test_broker.err = rc; + } +} + +static void +test_broker_handle(const uint8_t *pkt, size_t hdr_len, size_t len) +{ + const uint8_t *var = pkt + hdr_len; + uint8_t rsp[6]; + size_t topic_len; + int qos; + + switch (pkt[0] & 0xF0) { + case 0x10: + /* CONNECT -> CONNACK, no session, success, no properties */ + test_broker.connects++; + if (test_broker.close_on_connect) { + test_broker_close(); + return; + } + rsp[0] = 0x20; + rsp[1] = 3; + rsp[2] = 0; + rsp[3] = 0; + rsp[4] = 0; + test_broker_send(rsp, 5); + break; + case 0x80: + /* SUBSCRIBE -> SUBACK granting QoS 1 */ + rsp[0] = 0x90; + rsp[1] = 4; + rsp[2] = var[0]; + rsp[3] = var[1]; + rsp[4] = 0; + rsp[5] = 1; + test_broker_send(rsp, 6); + break; + case 0x30: + /* PUBLISH -> PUBACK (QoS 1), then deliver it back to the client */ + test_broker.publishes++; + qos = (pkt[0] >> 1) & 0x03; + if (qos) { + topic_len = (var[0] << 8) | var[1]; + rsp[0] = 0x40; + rsp[1] = 2; + rsp[2] = var[2 + topic_len]; + rsp[3] = var[3 + topic_len]; + test_broker_send(rsp, 4); + } + test_broker_send(pkt, len); + break; + case 0xC0: + /* PINGREQ -> PINGRESP */ + test_broker.pings++; + rsp[0] = 0xD0; + rsp[1] = 0; + test_broker_send(rsp, 2); + break; + case 0xE0: + test_broker.disconnects++; + test_broker_close(); + break; + case 0x40: + /* PUBACK for a delivered message */ + break; + default: + test_broker.err = -1; + test_broker_close(); + break; + } +} + +static void +test_broker_process(void) +{ + size_t rem_len; + size_t total; + size_t i; + int shift; + + while (test_broker.conn && test_broker.rx_len >= 2) { + rem_len = 0; + shift = 0; + i = 1; + do { + if (i >= test_broker.rx_len) { + return; + } + rem_len |= (test_broker.rx[i] & 0x7F) << shift; + shift += 7; + } while (test_broker.rx[i++] & 0x80 && i < 5); + + total = i + rem_len; + if (total > test_broker.rx_len) { + return; + } + + test_broker_handle(test_broker.rx, i, total); + if (!test_broker.conn) { + return; + } + + test_broker.rx_len -= total; + memmove(test_broker.rx, test_broker.rx + total, test_broker.rx_len); + } +} + +static void +test_broker_readable(void *arg, int err) +{ + struct os_mbuf *om; + uint16_t len; + int rc; + + while (test_broker.conn) { + rc = mn_recvfrom(test_broker.conn, &om, NULL); + if (rc == MN_EAGAIN) { + break; + } + if (rc) { + /* Client closed the connection */ + test_broker_close(); + break; + } + + len = OS_MBUF_PKTLEN(om); + if (test_broker.rx_len + len > sizeof(test_broker.rx)) { + os_mbuf_free_chain(om); + test_broker.err = MN_ENOBUFS; + test_broker_close(); + break; + } + os_mbuf_copydata(om, 0, len, test_broker.rx + test_broker.rx_len); + test_broker.rx_len += len; + os_mbuf_free_chain(om); + + test_broker_process(); + } +} + +static void +test_broker_writable(void *arg, int err) +{ +} + +static const union mn_socket_cb test_broker_conn_cbs = { + .socket = { + .readable = test_broker_readable, + .writable = test_broker_writable, + }, +}; + +static int +test_broker_newconn(void *arg, struct mn_socket *new) +{ + if (test_broker.conn) { + return -1; + } + + test_broker.conn = new; + test_broker.rx_len = 0; + mn_socket_set_cbs(new, NULL, &test_broker_conn_cbs); + + return 0; +} + +static const union mn_socket_cb test_broker_listen_cbs = { + .listen = { + .newconn = test_broker_newconn, + }, +}; + +void +test_broker_start(void) +{ + struct mn_sockaddr_in sin; + int rc; + + memset(&test_broker, 0, sizeof(test_broker)); + + rc = mn_socket(&test_broker.listen, MN_PF_INET, MN_SOCK_STREAM, 0); + TEST_ASSERT_FATAL(rc == 0); + mn_socket_set_cbs(test_broker.listen, NULL, &test_broker_listen_cbs); + + memset(&sin, 0, sizeof(sin)); + sin.msin_len = sizeof(sin); + sin.msin_family = MN_AF_INET; + sin.msin_port = htons(TEST_BROKER_PORT); + mn_inet_pton(MN_AF_INET, "127.0.0.1", &sin.msin_addr); + + rc = mn_bind(test_broker.listen, (struct mn_sockaddr *)&sin); + TEST_ASSERT_FATAL(rc == 0, "bind rc=%d", rc); + + rc = mn_listen(test_broker.listen, 2); + TEST_ASSERT_FATAL(rc == 0, "listen rc=%d", rc); +} + +void +test_broker_wait_idle(void) +{ + int i; + + for (i = 0; i < 100 && test_broker.conn; i++) { + os_time_delay(os_time_ms_to_ticks32(20)); + } + TEST_ASSERT_FATAL(test_broker.conn == NULL); +} diff --git a/net/mqtt/coremqtt/selftest/syscfg.yml b/net/mqtt/coremqtt/selftest/syscfg.yml new file mode 100644 index 000000000..e61d12e9a --- /dev/null +++ b/net/mqtt/coremqtt/selftest/syscfg.yml @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +syscfg.vals: + # Room for large publishes in both directions + MSYS_1_BLOCK_COUNT: 64 + MSYS_1_BLOCK_SIZE: 256 + # Host printf needs more stack than the test task has + COREMQTT_LOG_LVL: 4
