1 /*
2  * SPDX-License-Identifier: Apache-2.0
3  *
4  * Copyright (C) 2025, Joakim Andersson
5  */
6 
7 /**
8  * @addtogroup t_modem_driver
9  * @{
10  * @defgroup t_modem_cmd_handler test_modem_cmd_handler
11  * @}
12  */
13 
14 
15 #include <zephyr/ztest.h>
16 #include <zephyr/fff.h>
17 #include <modem_cmd_handler.h>
18 
19 DEFINE_FFF_GLOBALS;
20 
21 static struct modem_cmd_handler_data cmd_handler_data;
22 static struct modem_cmd_handler cmd_handler;
23 static struct modem_iface mock_modem_iface;
24 static struct k_sem sem_response;
25 #define MDM_RECV_BUF_SIZE 512
26 NET_BUF_POOL_DEFINE(_mdm_recv_pool, 10, MDM_RECV_BUF_SIZE, 0, NULL);
27 
28 uint8_t cmd_match_buf[MDM_RECV_BUF_SIZE + 1];
29 
MODEM_CMD_DEFINE(mock_on_cmd_ok)30 MODEM_CMD_DEFINE(mock_on_cmd_ok)
31 {
32 	modem_cmd_handler_set_error(data, 0);
33 	k_sem_give(&sem_response);
34 	return 0;
35 }
36 
MODEM_CMD_DEFINE(mock_on_cmd_error)37 MODEM_CMD_DEFINE(mock_on_cmd_error)
38 {
39 	modem_cmd_handler_set_error(data, -EIO);
40 	k_sem_give(&sem_response);
41 	return 0;
42 }
43 
44 static const struct modem_cmd mock_response_cmds[] = {
45 	MODEM_CMD("OK", mock_on_cmd_ok, 0U, ""), /* 3GPP */
46 	MODEM_CMD("ERROR", mock_on_cmd_error, 0U, ""), /* 3GPP */
47 };
48 
49 static const struct modem_cmd_handler_config cmd_handler_config = {
50 	.match_buf = &cmd_match_buf[0],
51 	.match_buf_len = sizeof(cmd_match_buf),
52 	.buf_pool = &_mdm_recv_pool,
53 	.alloc_timeout = K_NO_WAIT,
54 	.eol = "\r",
55 	.user_data = NULL,
56 	.response_cmds = mock_response_cmds,
57 	.response_cmds_len = ARRAY_SIZE(mock_response_cmds),
58 	.unsol_cmds = NULL,
59 	.unsol_cmds_len = 0,
60 };
61 
62 static const char *response_delayed;
63 size_t mock_write_expected_data_count;
64 static const char *mock_write_expected_data[10];
65 
66 FAKE_VALUE_FUNC(int, mock_read, struct modem_iface *, uint8_t *, size_t, size_t *);
67 FAKE_VALUE_FUNC(int, mock_write, struct modem_iface *, const uint8_t *, size_t);
68 FAKE_VALUE_FUNC(int, mock_on_response, struct modem_cmd_handler_data *, uint16_t,
69 		uint8_t **, uint16_t);
70 
mock_modem_iface_init(struct modem_iface * iface)71 static void mock_modem_iface_init(struct modem_iface *iface)
72 {
73 	iface->read = mock_read;
74 	iface->write = mock_write;
75 }
76 
mock_modem_iface_receive_data(struct modem_iface * iface,uint8_t * data,size_t len,size_t * bytes_read)77 static int mock_modem_iface_receive_data(struct modem_iface *iface,
78 					 uint8_t *data,
79 					 size_t len, size_t *bytes_read)
80 {
81 	if (!response_delayed) {
82 		*bytes_read = 0;
83 		return mock_read_fake.return_val;
84 	}
85 
86 	size_t response_delayed_len = strlen(response_delayed);
87 
88 	zassert_true(len > response_delayed_len, "Insufficient data length for response");
89 	memcpy(data, response_delayed, response_delayed_len);
90 	*bytes_read = response_delayed_len;
91 	response_delayed = NULL;
92 
93 	return mock_read_fake.return_val;
94 }
95 
_send_response_delayed_work(struct k_work * work)96 static void _send_response_delayed_work(struct k_work *work)
97 {
98 	mock_read_fake.custom_fake = mock_modem_iface_receive_data;
99 	modem_cmd_handler_process(&cmd_handler, &mock_modem_iface);
100 }
101 
102 K_WORK_DELAYABLE_DEFINE(response_delayable, _send_response_delayed_work);
103 
recv_data_delayed(const char * str,k_timeout_t delay)104 void recv_data_delayed(const char *str, k_timeout_t delay)
105 {
106 	response_delayed = str;
107 	k_work_schedule(&response_delayable, delay);
108 }
109 
modem_modem_iface_send_data(struct modem_iface * iface,const uint8_t * data,size_t len)110 static int modem_modem_iface_send_data(struct modem_iface *iface,
111 				       const uint8_t *data,
112 				       size_t len)
113 {
114 	zassert_true(strncmp((const char *)data,
115 			     mock_write_expected_data[mock_write_fake.call_count - 1], len) == 0,
116 				 "Sent command does not match expected");
117 	return mock_write_fake.return_val;
118 }
119 
send_data_verify(const char * expected_cmd)120 void send_data_verify(const char *expected_cmd)
121 {
122 	mock_write_expected_data[mock_write_expected_data_count++] = expected_cmd;
123 	mock_write_fake.custom_fake = modem_modem_iface_send_data;
124 }
125 
ZTEST(suite_modem_cmd_send,test_recv_ok)126 ZTEST(suite_modem_cmd_send, test_recv_ok)
127 {
128 	int ret = modem_cmd_handler_init(&cmd_handler,
129 					 &cmd_handler_data,
130 					 &cmd_handler_config);
131 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
132 
133 	send_data_verify("AT+INIT");
134 	send_data_verify("\r");
135 
136 	recv_data_delayed("OK\r", K_MSEC(100));
137 
138 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
139 			     NULL, 0U, "AT+INIT", &sem_response,
140 			     K_SECONDS(1));
141 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
142 }
143 
ZTEST(suite_modem_cmd_send,test_recv_error)144 ZTEST(suite_modem_cmd_send, test_recv_error)
145 {
146 	int ret = modem_cmd_handler_init(&cmd_handler,
147 					 &cmd_handler_data,
148 					 &cmd_handler_config);
149 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
150 
151 	send_data_verify("AT+INIT");
152 	send_data_verify("\r");
153 
154 	recv_data_delayed("ERROR\r", K_MSEC(100));
155 
156 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
157 			     NULL, 0U, "AT+INIT", &sem_response,
158 			     K_SECONDS(1));
159 	zassert_equal(ret, -EIO, "modem_cmd_send should return -EIO on error");
160 }
161 
ZTEST(suite_modem_cmd_send,test_recv_timeout)162 ZTEST(suite_modem_cmd_send, test_recv_timeout)
163 {
164 	int ret = modem_cmd_handler_init(&cmd_handler,
165 					 &cmd_handler_data,
166 					 &cmd_handler_config);
167 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
168 
169 	send_data_verify("AT+INIT");
170 	send_data_verify("\r");
171 
172 	/* No response is sent to trigger timeout */
173 
174 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
175 			     NULL, 0U, "AT+INIT", &sem_response,
176 			     K_SECONDS(1));
177 	zassert_equal(ret, -ETIMEDOUT, "modem_cmd_send should return -ETIMEDOUT on timeout");
178 }
179 
MODEM_CMD_DEFINE(on_cmd_response)180 MODEM_CMD_DEFINE(on_cmd_response)
181 {
182 	zassert_equal(argc, 1);
183 	zassert_equal(strcmp(argv[0], "123"), 0);
184 	return 0;
185 }
186 
ZTEST(suite_modem_cmd_send,test_recv_response)187 ZTEST(suite_modem_cmd_send, test_recv_response)
188 {
189 	int ret = modem_cmd_handler_init(&cmd_handler,
190 					 &cmd_handler_data,
191 					 &cmd_handler_config);
192 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
193 
194 	send_data_verify("AT+CMD");
195 	send_data_verify("\r");
196 
197 	recv_data_delayed("+CMD: 123\r"
198 			  "OK\r",
199 			  K_MSEC(100));
200 
201 	mock_on_response_fake.custom_fake = on_cmd_response;
202 	struct modem_cmd cmd[] = {
203 		MODEM_CMD("+CMD: ", mock_on_response, 1, ""),
204 	};
205 
206 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
207 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
208 			     K_SECONDS(1));
209 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
210 	zassert_equal(mock_on_response_fake.call_count, 1);
211 }
212 
MODEM_CMD_DEFINE(on_cmd_response_parse_args)213 MODEM_CMD_DEFINE(on_cmd_response_parse_args)
214 {
215 	zassert_equal(argc, 4);
216 	zassert_equal(strcmp(argv[0], "1"), 0);
217 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
218 	zassert_equal(strcmp(argv[2], "\"three\""), 0);
219 	zassert_equal(strcmp(argv[3], "4"), 0);
220 	return 0;
221 }
222 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args)223 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args)
224 {
225 	int ret = modem_cmd_handler_init(&cmd_handler,
226 					 &cmd_handler_data,
227 					 &cmd_handler_config);
228 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
229 
230 	send_data_verify("AT+CMD");
231 	send_data_verify("\r");
232 
233 	recv_data_delayed("+CMD: 1,\"two\",\"three\",4\r"
234 			  "OK\r",
235 			  K_MSEC(100));
236 
237 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args;
238 	struct modem_cmd cmd[] = {
239 		MODEM_CMD("+CMD: ", mock_on_response, 4, ","),
240 	};
241 
242 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
243 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
244 			     K_SECONDS(1));
245 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
246 	zassert_equal(mock_on_response_fake.call_count, 1);
247 }
248 
MODEM_CMD_DEFINE(on_cmd_response_parse_args_quoted_delim)249 MODEM_CMD_DEFINE(on_cmd_response_parse_args_quoted_delim)
250 {
251 	zassert_equal(argc, 4);
252 	zassert_equal(strcmp(argv[0], "1"), 0);
253 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
254 	zassert_equal(strcmp(argv[2], "\"thr,ee\""), 0);
255 	zassert_equal(strcmp(argv[3], "4"), 0);
256 	return 0;
257 }
258 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args_quoted_delim)259 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args_quoted_delim)
260 {
261 	int ret = modem_cmd_handler_init(&cmd_handler,
262 					 &cmd_handler_data,
263 					 &cmd_handler_config);
264 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
265 
266 	send_data_verify("AT+CMD");
267 	send_data_verify("\r");
268 
269 	recv_data_delayed("+CMD: 1,\"two\",\"thr,ee\",4\r"
270 			  "OK\r",
271 			  K_MSEC(100));
272 
273 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args_quoted_delim;
274 	struct modem_cmd cmd[] = {
275 		MODEM_CMD("+CMD: ", mock_on_response, 4, ","),
276 	};
277 
278 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
279 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
280 			     K_SECONDS(1));
281 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
282 	zassert_equal(mock_on_response_fake.call_count, 1);
283 }
284 
MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg)285 MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg)
286 {
287 	zassert_equal(argc, 4);
288 	zassert_equal(strcmp(argv[0], "1"), 0);
289 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
290 	zassert_equal(strcmp(argv[2], ""), 0);
291 	zassert_equal(strcmp(argv[3], "4"), 0);
292 	return 0;
293 }
294 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args_empty_arg)295 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args_empty_arg)
296 {
297 	int ret = modem_cmd_handler_init(&cmd_handler,
298 					 &cmd_handler_data,
299 					 &cmd_handler_config);
300 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
301 
302 	send_data_verify("AT+CMD");
303 	send_data_verify("\r");
304 
305 	recv_data_delayed("+CMD: 1,\"two\",,4\r"
306 			  "OK\r",
307 			  K_MSEC(100));
308 
309 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args_empty_arg;
310 	struct modem_cmd cmd[] = {
311 		MODEM_CMD("+CMD: ", mock_on_response, 4, ","),
312 	};
313 
314 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
315 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
316 			     K_SECONDS(1));
317 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
318 	zassert_equal(mock_on_response_fake.call_count, 1);
319 }
320 
MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg_end)321 MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg_end)
322 {
323 	zassert_equal(argc, 4);
324 	zassert_equal(strcmp(argv[0], "1"), 0);
325 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
326 	zassert_equal(strcmp(argv[2], "\"three\""), 0);
327 	zassert_equal(strcmp(argv[3], ""), 0);
328 	return 0;
329 }
330 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args_empty_arg_end)331 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args_empty_arg_end)
332 {
333 	int ret = modem_cmd_handler_init(&cmd_handler,
334 					 &cmd_handler_data,
335 					 &cmd_handler_config);
336 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
337 
338 	send_data_verify("AT+CMD");
339 	send_data_verify("\r");
340 
341 	recv_data_delayed("+CMD: 1,\"two\",\"three\",\r"
342 			  "OK\r",
343 			  K_MSEC(100));
344 
345 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args_empty_arg_end;
346 	struct modem_cmd cmd[] = {
347 		MODEM_CMD("+CMD: ", mock_on_response, 4, ","),
348 	};
349 
350 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
351 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
352 			     K_SECONDS(1));
353 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
354 	zassert_equal(mock_on_response_fake.call_count, 1);
355 }
356 
MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg_begin)357 MODEM_CMD_DEFINE(on_cmd_response_parse_args_empty_arg_begin)
358 {
359 	zassert_equal(argc, 4);
360 	zassert_equal(strcmp(argv[0], ""), 0);
361 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
362 	zassert_equal(strcmp(argv[2], "\"three\""), 0);
363 	zassert_equal(strcmp(argv[3], "4"), 0);
364 	return 0;
365 }
366 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args_empty_arg_begin)367 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args_empty_arg_begin)
368 {
369 	int ret = modem_cmd_handler_init(&cmd_handler,
370 					 &cmd_handler_data,
371 					 &cmd_handler_config);
372 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
373 
374 	send_data_verify("AT+CMD");
375 	send_data_verify("\r");
376 
377 	recv_data_delayed("+CMD: ,\"two\",\"three\",4\r"
378 			  "OK\r",
379 			  K_MSEC(100));
380 
381 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args_empty_arg_begin;
382 	struct modem_cmd cmd[] = {
383 		MODEM_CMD("+CMD: ", mock_on_response, 4, ","),
384 	};
385 
386 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
387 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
388 			     K_SECONDS(1));
389 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
390 	zassert_equal(mock_on_response_fake.call_count, 1);
391 }
392 
MODEM_CMD_DEFINE(on_cmd_response_parse_args_multi_delim)393 MODEM_CMD_DEFINE(on_cmd_response_parse_args_multi_delim)
394 {
395 	zassert_equal(argc, 4);
396 	zassert_equal(strcmp(argv[0], "1"), 0);
397 	zassert_equal(strcmp(argv[1], "\"two\""), 0);
398 	zassert_equal(strcmp(argv[2], "\"three\""), 0);
399 	zassert_equal(strcmp(argv[3], "4"), 0);
400 	return 0;
401 }
402 
ZTEST(suite_modem_cmd_send,test_recv_response_parse_args_multi_delim)403 ZTEST(suite_modem_cmd_send, test_recv_response_parse_args_multi_delim)
404 {
405 	int ret = modem_cmd_handler_init(&cmd_handler,
406 					 &cmd_handler_data,
407 					 &cmd_handler_config);
408 	zassert_equal(ret, 0, "modem_cmd_handler_init should return 0 on success");
409 
410 	send_data_verify("AT+CMD");
411 	send_data_verify("\r");
412 
413 	recv_data_delayed("+CMD: 1:\"two\";\"three\",4\r"
414 			  "OK\r",
415 			  K_MSEC(100));
416 
417 	mock_on_response_fake.custom_fake = on_cmd_response_parse_args_multi_delim;
418 	struct modem_cmd cmd[] = {
419 		MODEM_CMD("+CMD: ", mock_on_response, 4, ",;:"),
420 	};
421 
422 	ret = modem_cmd_send(&mock_modem_iface, &cmd_handler,
423 			     cmd, ARRAY_SIZE(cmd), "AT+CMD", &sem_response,
424 			     K_SECONDS(1));
425 	zassert_equal(ret, 0, "modem_cmd_send should return 0 on success");
426 	zassert_equal(mock_on_response_fake.call_count, 1);
427 }
428 
test_setup(void * fixture)429 static void test_setup(void *fixture)
430 {
431 	mock_write_expected_data_count = 0;
432 
433 	RESET_FAKE(mock_read);
434 	RESET_FAKE(mock_write);
435 	RESET_FAKE(mock_on_response);
436 
437 	k_sem_init(&sem_response, 0, 1);
438 
439 	mock_modem_iface_init(&mock_modem_iface);
440 }
441 
test_teardown(void * fixture)442 static void test_teardown(void *fixture)
443 {
444 	zassert_equal(mock_write_fake.call_count, mock_write_expected_data_count,
445 		     "Not all expected commands were sent");
446 }
447 
448 ZTEST_SUITE(suite_modem_cmd_send, NULL, NULL, test_setup, test_teardown, NULL);
449