xref: /linux/drivers/net/wireless/intel/iwlwifi/mld/tests/link-selection.c (revision a153825987be45a6a22fdf3bb4ae59109818c62b)
1 // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
2 /*
3  * KUnit tests for link selection functions
4  *
5  * Copyright (C) 2025-2026 Intel Corporation
6  */
7 #include <kunit/static_stub.h>
8 
9 #include "utils.h"
10 #include "mld.h"
11 #include "link.h"
12 #include "iface.h"
13 #include "phy.h"
14 #include "mlo.h"
15 
16 struct link_grading_input {
17 	u8 link_id;
18 	const struct cfg80211_chan_def *chandef;
19 	bool active;
20 	s32 signal;
21 	bool has_chan_util_elem;
22 	u8 chan_util;
23 	u8 chan_load_by_us;
24 	s8 dup_beacon_adj;
25 	s8 psd_eirp_adj;
26 	u16 punctured;
27 };
28 
29 static const struct link_grading_test_case {
30 	const char *desc;
31 	struct link_grading_input link;
32 	unsigned int expected_grade;
33 } link_grading_cases[] = {
34 	/* Per-bandwidth grading table tests */
35 	{
36 		.desc = "20 MHz grading table: -75 dBm",
37 		.link = {
38 			.link_id = 0,
39 			.chandef = &chandef_2ghz_20mhz,
40 			.active = false,
41 			.signal = -75,
42 		},
43 		/* 137 * 0.7 (default 2.4GHz channel load 30%) */
44 		.expected_grade = 96,
45 	},
46 	{
47 		.desc = "20 MHz with channel util 128 (50%): -70 dBm",
48 		.link = {
49 			.link_id = 0,
50 			.chandef = &chandef_2ghz_20mhz,
51 			.active = false,
52 			.signal = -70,
53 			.has_chan_util_elem = true,
54 			.chan_util = 128,
55 		},
56 		.expected_grade = 86,
57 	},
58 	{
59 		.desc = "20 MHz with channel util 180 (70%): -70 dBm",
60 		.link = {
61 			.link_id = 0,
62 			.chandef = &chandef_2ghz_20mhz,
63 			.active = false,
64 			.signal = -70,
65 			.has_chan_util_elem = true,
66 			.chan_util = 180,
67 		},
68 		.expected_grade = 51,
69 	},
70 	{
71 		.desc = "20 MHz active link with chan load by us 10%: -70 dBm",
72 		.link = {
73 			.link_id = 0,
74 			.chandef = &chandef_2ghz_20mhz,
75 			.active = true,
76 			.signal = -70,
77 			.has_chan_util_elem = true,
78 			.chan_util = 180,
79 			.chan_load_by_us = 10,
80 		},
81 		.expected_grade = 67,
82 	},
83 	{
84 		.desc = "40 MHz grading table: -80 dBm",
85 		.link = {
86 			.link_id = 0,
87 			.chandef = &chandef_5ghz_40mhz,
88 			.active = false,
89 			.signal = -80,
90 		},
91 		/* 206 * 0.85 (default 5GHz channel load 15%) */
92 		.expected_grade = 175,
93 	},
94 	{
95 		.desc = "80 MHz grading table: -70 dBm",
96 		.link = {
97 			.link_id = 0,
98 			.chandef = &chandef_5ghz_80mhz,
99 			.active = false,
100 			.signal = -70,
101 		},
102 		/* 548 * 0.85 (default 5GHz channel load 15%) */
103 		.expected_grade = 466,
104 	},
105 	{
106 		.desc = "160 MHz grading table: -65 dBm",
107 		.link = {
108 			.link_id = 0,
109 			.chandef = &chandef_5ghz_160mhz,
110 			.active = false,
111 			.signal = -65,
112 		},
113 		/* 1240 * 0.85 (default 5GHz channel load 15%) */
114 		.expected_grade = 1055,
115 	},
116 	{
117 		.desc = "320 MHz grading table: -60 dBm",
118 		.link = {
119 			.link_id = 0,
120 			.chandef = &chandef_6ghz_320mhz,
121 			.active = false,
122 			.signal = -60,
123 		},
124 		/* 3680 at -56 dBm (-60 + 4 dBm 6 GHz) */
125 		.expected_grade = 3680,
126 	},
127 	/* 6 GHz RSSI adjustment integration tests */
128 	{
129 		.desc = "6 GHz 160 MHz with fixed +4 dBm adjustment",
130 		.link = {
131 			.link_id = 0,
132 			.chandef = &chandef_6ghz_160mhz,
133 			.active = false,
134 			.signal = -69,
135 		},
136 		/* -69 + 4 dBm = -65, grade 1240 */
137 		.expected_grade = 1240,
138 	},
139 	{
140 		.desc = "6 GHz 80 MHz with fixed +4 dBm adjustment",
141 		.link = {
142 			.link_id = 0,
143 			.chandef = &chandef_6ghz_80mhz,
144 			.active = false,
145 			.signal = -74,
146 		},
147 		/* -74 + 4 dBm = -70, grade 548 */
148 		.expected_grade = 548,
149 	},
150 	{
151 		.desc = "6 GHz 40 MHz with fixed +4 dBm adjustment",
152 		.link = {
153 			.link_id = 0,
154 			.chandef = &chandef_6ghz_40mhz,
155 			.active = false,
156 			.signal = -84,
157 		},
158 		/* -84 + 4 dBm = -80, grade 206 */
159 		.expected_grade = 206,
160 	},
161 	{
162 		.desc = "6 GHz 20 MHz with fixed +4 dBm adjustment",
163 		.link = {
164 			.link_id = 0,
165 			.chandef = &chandef_6ghz_20mhz,
166 			.active = false,
167 			.signal = -79,
168 		},
169 		.expected_grade = 137,
170 	},
171 	/* Duplicated beacon RSSI adjustment tests */
172 	{
173 		.desc = "6 GHz 40 MHz dup beacon: -81 dBm + 3 dBm = -78 dBm",
174 		.link = {
175 			.link_id = 0,
176 			.chandef = &chandef_6ghz_40mhz,
177 			.active = false,
178 			.signal = -81,
179 			.dup_beacon_adj = 3,
180 		},
181 		.expected_grade = 206,
182 	},
183 	{
184 		.desc = "6 GHz 80 MHz dup beacon: -73 dBm + 6 dBm = -67 dBm",
185 		.link = {
186 			.link_id = 0,
187 			.chandef = &chandef_6ghz_80mhz,
188 			.active = false,
189 			.signal = -73,
190 			.dup_beacon_adj = 6,
191 		},
192 		.expected_grade = 620,
193 	},
194 	{
195 		.desc = "6 GHz 160 MHz dup beacon: -74 dBm + 9 dBm = -65 dBm",
196 		.link = {
197 			.link_id = 0,
198 			.chandef = &chandef_6ghz_160mhz,
199 			.active = false,
200 			.signal = -74,
201 			.dup_beacon_adj = 9,
202 		},
203 		.expected_grade = 1240,
204 	},
205 	{
206 		.desc = "6 GHz 320 MHz dup beacon: -72 dBm + 12 dBm = -60 dBm",
207 		.link = {
208 			.link_id = 0,
209 			.chandef = &chandef_6ghz_320mhz,
210 			.active = false,
211 			.signal = -72,
212 			.dup_beacon_adj = 12,
213 		},
214 		.expected_grade = 3296,
215 	},
216 	/* PSD/EIRP RSSI adjustment tests */
217 	{
218 		.desc = "6 GHz 80 MHz PSD/EIRP: -77 dBm + 3 dBm = -74 dBm",
219 		.link = {
220 			.link_id = 0,
221 			.chandef = &chandef_6ghz_80mhz,
222 			.active = false,
223 			.signal = -77,
224 			.psd_eirp_adj = 3,
225 		},
226 		/* -77 + 3 dBm = -74, grade 412; fallback +4: -73 -> 548 */
227 		.expected_grade = 412,
228 	},
229 	{
230 		.desc = "6 GHz 160 MHz PSD/EIRP: -70 dBm + 3 dBm = -67 dBm",
231 		.link = {
232 			.link_id = 0,
233 			.chandef = &chandef_6ghz_160mhz,
234 			.active = false,
235 			.signal = -70,
236 			.psd_eirp_adj = 3,
237 		},
238 		/* -70 + 3 dBm = -67, grade 1096; fallback +4: -66 -> 1240 */
239 		.expected_grade = 1096,
240 	},
241 	/* Puncturing penalty tests */
242 	{
243 		.desc = "80 MHz with 20 MHz punctured: 3 active subchannels",
244 		.link = {
245 			.link_id = 0,
246 			.chandef = &chandef_5ghz_80mhz,
247 			.active = false,
248 			.signal = -70,
249 			.punctured = 0x2,
250 		},
251 		/* 548 * 0.85 (5GHz load) * 3/4 (puncturing) */
252 		.expected_grade = 349,
253 	},
254 	{
255 		.desc = "160 MHz with 40 MHz punctured: 6 active subchannels",
256 		.link = {
257 			.link_id = 0,
258 			.chandef = &chandef_5ghz_160mhz,
259 			.active = false,
260 			.signal = -65,
261 			.punctured = 0xC,
262 		},
263 		/* 1240 * 0.85 (5GHz load) * 6/8 (puncturing) */
264 		.expected_grade = 791,
265 	},
266 };
267 
268 KUNIT_ARRAY_PARAM_DESC(link_grading, link_grading_cases, desc);
269 
270 static s8 fake_dup_beacon_rssi_adjust(struct iwl_mld *mld,
271 				      struct ieee80211_bss_conf *link_conf)
272 {
273 	const struct link_grading_test_case *params =
274 		kunit_get_current_test()->param_value;
275 
276 	return params->link.dup_beacon_adj;
277 }
278 
279 static s8 fake_psd_eirp_rssi_adjust(struct ieee80211_bss_conf *link_conf)
280 {
281 	const struct link_grading_test_case *params =
282 		kunit_get_current_test()->param_value;
283 
284 	return params->link.psd_eirp_adj;
285 }
286 
287 static void setup_link(struct ieee80211_bss_conf *link)
288 {
289 	struct kunit *test = kunit_get_current_test();
290 	struct iwl_mld *mld = test->priv;
291 	const struct link_grading_test_case *test_param =
292 		(const void *)(test->param_value);
293 
294 	KUNIT_ALLOC_AND_ASSERT(test, link->bss);
295 
296 	link->bss->signal = DBM_TO_MBM(test_param->link.signal);
297 
298 	link->chanreq.oper = *test_param->link.chandef;
299 
300 	if (test_param->link.has_chan_util_elem) {
301 		struct cfg80211_bss_ies *ies;
302 		struct ieee80211_bss_load_elem bss_load = {
303 			.channel_util = test_param->link.chan_util,
304 		};
305 		struct element *elem =
306 			iwlmld_kunit_gen_element(WLAN_EID_QBSS_LOAD,
307 						 &bss_load,
308 						 sizeof(bss_load));
309 		unsigned int elem_len = sizeof(*elem) + sizeof(bss_load);
310 
311 		KUNIT_ALLOC_AND_ASSERT_SIZE(test, ies, sizeof(*ies) + elem_len);
312 		memcpy(ies->data, elem, elem_len);
313 		ies->len = elem_len;
314 		rcu_assign_pointer(link->bss->beacon_ies, ies);
315 		rcu_assign_pointer(link->bss->ies, ies);
316 	}
317 
318 	if (test_param->link.punctured)
319 		link->chanreq.oper.punctured = test_param->link.punctured;
320 
321 	if (test_param->link.active) {
322 		struct ieee80211_chanctx_conf *chan_ctx =
323 			wiphy_dereference(mld->wiphy, link->chanctx_conf);
324 		struct iwl_mld_phy *phy;
325 
326 		KUNIT_ASSERT_NOT_NULL(test, chan_ctx);
327 
328 		phy = iwl_mld_phy_from_mac80211(chan_ctx);
329 
330 		phy->channel_load_by_us = test_param->link.chan_load_by_us;
331 	}
332 }
333 
334 static void test_link_grading(struct kunit *test)
335 {
336 	struct iwl_mld *mld = test->priv;
337 	const struct link_grading_test_case *test_param =
338 		(const void *)(test->param_value);
339 	struct ieee80211_vif *vif;
340 	struct ieee80211_bss_conf *link;
341 	unsigned int actual_grade;
342 	u8 link_id = test_param->link.link_id;
343 	bool active = test_param->link.active;
344 	u16 valid_links;
345 	struct iwl_mld_kunit_link assoc_link = {
346 		.chandef = test_param->link.chandef,
347 	};
348 
349 	/* If the link is not active, use a different link as the assoc link */
350 	if (active) {
351 		assoc_link.id = link_id;
352 		valid_links = BIT(link_id);
353 	} else {
354 		assoc_link.id = BIT(ffz(BIT(link_id)));
355 		valid_links = BIT(assoc_link.id) | BIT(link_id);
356 	}
357 
358 	vif = iwlmld_kunit_setup_mlo_assoc(valid_links, &assoc_link);
359 
360 	kunit_activate_static_stub(test, iwl_mld_get_dup_beacon_rssi_adjust,
361 				   fake_dup_beacon_rssi_adjust);
362 	kunit_activate_static_stub(test, iwl_mld_get_psd_eirp_rssi_adjust,
363 				   fake_psd_eirp_rssi_adjust);
364 
365 	wiphy_lock(mld->wiphy);
366 	link = wiphy_dereference(mld->wiphy, vif->link_conf[link_id]);
367 	KUNIT_ASSERT_NOT_NULL(test, link);
368 
369 	setup_link(link);
370 
371 	actual_grade = iwl_mld_get_link_grade(mld, link);
372 	wiphy_unlock(mld->wiphy);
373 
374 	/* Assert that the returned grade matches the expected grade */
375 	KUNIT_EXPECT_EQ(test, actual_grade, test_param->expected_grade);
376 }
377 
378 static struct kunit_case link_selection_cases[] = {
379 	KUNIT_CASE_PARAM(test_link_grading, link_grading_gen_params),
380 	{},
381 };
382 
383 static struct kunit_suite link_selection = {
384 	.name = "iwlmld-link-selection-tests",
385 	.test_cases = link_selection_cases,
386 	.init = iwlmld_kunit_test_init,
387 };
388 
389 kunit_test_suite(link_selection);
390 
391 static const struct link_pair_case {
392 	const char *desc;
393 	const struct cfg80211_chan_def *chandef_a, *chandef_b;
394 	bool low_latency_vif;
395 	u32 chan_load_not_by_us;
396 	bool primary_link_active;
397 	u32 expected_result;
398 } link_pair_cases[] = {
399 	{
400 		.desc = "Unequal bandwidth, primary link inactive, EMLSR not allowed",
401 		.low_latency_vif = false,
402 		.primary_link_active = false,
403 		.chandef_a = &chandef_5ghz_40mhz,
404 		.chandef_b = &chandef_6ghz_20mhz,
405 		.expected_result = IWL_MLD_EMLSR_EXIT_CHAN_LOAD,
406 	},
407 	{
408 		.desc = "Equal bandwidths, sufficient channel load, EMLSR allowed",
409 		.low_latency_vif = false,
410 		.primary_link_active = true,
411 		.chan_load_not_by_us = 11,
412 		.chandef_a = &chandef_5ghz_40mhz,
413 		.chandef_b = &chandef_6ghz_40mhz,
414 		.expected_result = 0,
415 	},
416 	{
417 		.desc = "Equal bandwidths, insufficient channel load, EMLSR not allowed",
418 		.low_latency_vif = false,
419 		.primary_link_active = true,
420 		.chan_load_not_by_us = 6,
421 		.chandef_a = &chandef_5ghz_80mhz,
422 		.chandef_b = &chandef_6ghz_80mhz,
423 		.expected_result = IWL_MLD_EMLSR_EXIT_CHAN_LOAD,
424 	},
425 	{
426 		.desc = "Low latency VIF, sufficient channel load, EMLSR allowed",
427 		.low_latency_vif = true,
428 		.primary_link_active = true,
429 		.chan_load_not_by_us = 6,
430 		.chandef_a = &chandef_5ghz_160mhz,
431 		.chandef_b = &chandef_6ghz_160mhz,
432 		.expected_result = 0,
433 	},
434 	{
435 		.desc = "Different bandwidths (2x ratio), primary link load permits EMLSR",
436 		.low_latency_vif = false,
437 		.primary_link_active = true,
438 		.chan_load_not_by_us = 30,
439 		.chandef_a = &chandef_5ghz_40mhz,
440 		.chandef_b = &chandef_6ghz_20mhz,
441 		.expected_result = 0,
442 	},
443 	{
444 		.desc = "Different bandwidths (4x ratio), primary link load permits EMLSR",
445 		.low_latency_vif = false,
446 		.primary_link_active = true,
447 		.chan_load_not_by_us = 45,
448 		.chandef_a = &chandef_5ghz_80mhz,
449 		.chandef_b = &chandef_6ghz_20mhz,
450 		.expected_result = 0,
451 	},
452 	{
453 		.desc = "Different bandwidths (16x ratio), primary link load insufficient",
454 		.low_latency_vif = false,
455 		.primary_link_active = true,
456 		.chan_load_not_by_us = 45,
457 		.chandef_a = &chandef_6ghz_320mhz,
458 		.chandef_b = &chandef_5ghz_20mhz,
459 		.expected_result = IWL_MLD_EMLSR_EXIT_CHAN_LOAD,
460 	},
461 	{
462 		.desc = "Same band not allowed (2.4 GHz)",
463 		.low_latency_vif = false,
464 		.primary_link_active = true,
465 		.chan_load_not_by_us = 30,
466 		.chandef_a = &chandef_2ghz_20mhz,
467 		.chandef_b = &chandef_2ghz_11_20mhz,
468 		.expected_result = IWL_MLD_EMLSR_EXIT_EQUAL_BAND,
469 	},
470 	{
471 		.desc = "Same band not allowed (5 GHz)",
472 		.low_latency_vif = false,
473 		.primary_link_active = true,
474 		.chan_load_not_by_us = 30,
475 		.chandef_a = &chandef_5ghz_40mhz,
476 		.chandef_b = &chandef_5ghz_40mhz,
477 		.expected_result = IWL_MLD_EMLSR_EXIT_EQUAL_BAND,
478 	},
479 	{
480 		.desc = "Same band allowed (5 GHz separated)",
481 		.low_latency_vif = false,
482 		.primary_link_active = true,
483 		.chan_load_not_by_us = 30,
484 		.chandef_a = &chandef_5ghz_40mhz,
485 		.chandef_b = &chandef_5ghz_120_40mhz,
486 		.expected_result = 0,
487 	},
488 	{
489 		.desc = "Same band not allowed (6 GHz)",
490 		.low_latency_vif = false,
491 		.primary_link_active = true,
492 		.chan_load_not_by_us = 30,
493 		.chandef_a = &chandef_6ghz_160mhz,
494 		.chandef_b = &chandef_6ghz_221_160mhz,
495 		.expected_result = IWL_MLD_EMLSR_EXIT_EQUAL_BAND,
496 	},
497 };
498 
499 KUNIT_ARRAY_PARAM_DESC(link_pair, link_pair_cases, desc);
500 
501 static void test_iwl_mld_link_pair_allows_emlsr(struct kunit *test)
502 {
503 	const struct link_pair_case *params = test->param_value;
504 	struct iwl_mld *mld = test->priv;
505 	struct ieee80211_vif *vif;
506 	/* link A is the primary and link B is the secondary */
507 	struct iwl_mld_link_sel_data a = {
508 		.chandef = params->chandef_a,
509 		.link_id = 4,
510 	};
511 	struct iwl_mld_link_sel_data b = {
512 		.chandef = params->chandef_b,
513 		.link_id = 5,
514 	};
515 	struct iwl_mld_kunit_link assoc_link = {
516 		.chandef = params->primary_link_active ? a.chandef : b.chandef,
517 		.id = params->primary_link_active ? a.link_id : b.link_id,
518 	};
519 	u32 result;
520 
521 	vif = iwlmld_kunit_setup_mlo_assoc(BIT(a.link_id) | BIT(b.link_id),
522 					   &assoc_link);
523 
524 	if (params->low_latency_vif)
525 		iwl_mld_vif_from_mac80211(vif)->low_latency_causes = 1;
526 
527 	wiphy_lock(mld->wiphy);
528 
529 	/* Simulate channel load */
530 	if (params->primary_link_active) {
531 		struct iwl_mld_phy *phy =
532 			iwlmld_kunit_get_phy_of_link(vif, a.link_id);
533 
534 		phy->avg_channel_load_not_by_us = params->chan_load_not_by_us;
535 	}
536 
537 	result = iwl_mld_emlsr_pair_state(vif, &a, &b);
538 
539 	wiphy_unlock(mld->wiphy);
540 
541 	KUNIT_EXPECT_EQ(test, result, params->expected_result);
542 }
543 
544 static struct kunit_case link_pair_criteria_test_cases[] = {
545 	KUNIT_CASE_PARAM(test_iwl_mld_link_pair_allows_emlsr, link_pair_gen_params),
546 	{}
547 };
548 
549 static struct kunit_suite link_pair_criteria_tests = {
550 	.name = "iwlmld_link_pair_allows_emlsr",
551 	.test_cases = link_pair_criteria_test_cases,
552 	.init = iwlmld_kunit_test_init,
553 };
554 
555 kunit_test_suite(link_pair_criteria_tests);
556