| 1 | |
| 2 | |
| 3 |
|
| 4 | |
| 5 | API level tests for RSS (mostly Netlink vs IOCTL).
|
| 6 |
|
| 7 |
|
| 8 | import errno
|
| 9 | import glob
|
| 10 | import random
|
| 11 | from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises
|
| 12 | from lib.py import KsftSkipEx, KsftFailEx
|
| 13 | from lib.py import defer, ethtool, CmdExitFailure
|
| 14 | from lib.py import EthtoolFamily, NlError
|
| 15 | from lib.py import NetDrvEnv
|
| 16 |
|
| 17 |
|
| 18 | def _require_2qs(cfg):
|
| 19 | qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
|
| 20 | if qcnt < 2:
|
| 21 | raise KsftSkipEx(f"Local has only {qcnt} queues")
|
| 22 | return qcnt
|
| 23 |
|
| 24 |
|
| 25 | def _ethtool_create(cfg, act, opts):
|
| 26 | output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
|
| 27 | |
| 28 | |
| 29 | return int(output.split()[-1])
|
| 30 |
|
| 31 |
|
| 32 | def _ethtool_get_cfg(cfg, fl_type, to_nl=False):
|
| 33 | descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout
|
| 34 |
|
| 35 | if to_nl:
|
| 36 | converter = {
|
| 37 | "IP SA": "ip-src",
|
| 38 | "IP DA": "ip-dst",
|
| 39 | "L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1",
|
| 40 | "L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3",
|
| 41 | }
|
| 42 |
|
| 43 | ret = set()
|
| 44 | else:
|
| 45 | converter = {
|
| 46 | "IP SA": "s",
|
| 47 | "IP DA": "d",
|
| 48 | "L3 proto": "t",
|
| 49 | "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
|
| 50 | "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
|
| 51 | }
|
| 52 |
|
| 53 | ret = ""
|
| 54 |
|
| 55 | for line in descr.split("\n")[1:-2]:
|
| 56 | |
| 57 | if to_nl:
|
| 58 | ret.add(converter[line])
|
| 59 | else:
|
| 60 | ret += converter[line]
|
| 61 | return ret
|
| 62 |
|
| 63 |
|
| 64 | def test_rxfh_nl_set_fail(cfg):
|
| 65 | |
| 66 | Test error path of Netlink SET.
|
| 67 |
|
| 68 | _require_2qs(cfg)
|
| 69 |
|
| 70 | ethnl = EthtoolFamily()
|
| 71 | ethnl.ntf_subscribe("monitor")
|
| 72 |
|
| 73 | with ksft_raises(NlError):
|
| 74 | ethnl.rss_set({"header": {"dev-name": "lo"},
|
| 75 | "indir": None})
|
| 76 |
|
| 77 | with ksft_raises(NlError):
|
| 78 | ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 79 | "indir": [100000]})
|
| 80 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 81 | ksft_is(ntf, None)
|
| 82 |
|
| 83 |
|
| 84 | def test_rxfh_nl_set_indir(cfg):
|
| 85 | |
| 86 | Test setting indirection table via Netlink.
|
| 87 |
|
| 88 | qcnt = _require_2qs(cfg)
|
| 89 |
|
| 90 | |
| 91 | reset = defer(cfg.ethnl.rss_set,
|
| 92 | {"header": {"dev-index": cfg.ifindex}, "indir": None})
|
| 93 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 94 | "indir": [1]})
|
| 95 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 96 | ksft_eq(set(rss.get("indir", [-1])), {1})
|
| 97 |
|
| 98 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 99 | "indir": [0, 1]})
|
| 100 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 101 | ksft_eq(set(rss.get("indir", [-1])), {0, 1})
|
| 102 |
|
| 103 | |
| 104 | with ksft_raises(CmdExitFailure):
|
| 105 | ethtool(f"-L {cfg.ifname} combined 0 rx 1")
|
| 106 | with ksft_raises(CmdExitFailure):
|
| 107 | ethtool(f"-L {cfg.ifname} combined 1 rx 0")
|
| 108 |
|
| 109 | |
| 110 | reset.exec()
|
| 111 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 112 | ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt)))
|
| 113 |
|
| 114 |
|
| 115 | def test_rxfh_nl_set_indir_ctx(cfg):
|
| 116 | |
| 117 | Test setting indirection table for a custom context via Netlink.
|
| 118 |
|
| 119 | _require_2qs(cfg)
|
| 120 |
|
| 121 | |
| 122 | dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 123 |
|
| 124 | |
| 125 | ctx_id = _ethtool_create(cfg, "-X", "context new")
|
| 126 | defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
|
| 127 |
|
| 128 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 129 | "context": ctx_id, "indir": [1]})
|
| 130 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
|
| 131 | "context": ctx_id})
|
| 132 | ksft_eq(set(rss.get("indir", [-1])), {1})
|
| 133 |
|
| 134 | ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 135 | ksft_eq(ctx0, dflt)
|
| 136 |
|
| 137 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 138 | "context": ctx_id, "indir": [0, 1]})
|
| 139 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
|
| 140 | "context": ctx_id})
|
| 141 | ksft_eq(set(rss.get("indir", [-1])), {0, 1})
|
| 142 |
|
| 143 | ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 144 | ksft_eq(ctx0, dflt)
|
| 145 |
|
| 146 | |
| 147 | with ksft_raises(CmdExitFailure):
|
| 148 | ethtool(f"-L {cfg.ifname} combined 0 rx 1")
|
| 149 | with ksft_raises(CmdExitFailure):
|
| 150 | ethtool(f"-L {cfg.ifname} combined 1 rx 0")
|
| 151 |
|
| 152 |
|
| 153 | def test_rxfh_indir_ntf(cfg):
|
| 154 | |
| 155 | Check that Netlink notifications are generated when RSS indirection
|
| 156 | table was modified.
|
| 157 |
|
| 158 | _require_2qs(cfg)
|
| 159 |
|
| 160 | ethnl = EthtoolFamily()
|
| 161 | ethnl.ntf_subscribe("monitor")
|
| 162 |
|
| 163 | ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1")
|
| 164 | reset = defer(ethtool, f"-X {cfg.ifname} default")
|
| 165 |
|
| 166 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 167 | if ntf is None:
|
| 168 | raise KsftFailEx("No notification received")
|
| 169 | ksft_eq(ntf["name"], "rss-ntf")
|
| 170 | ksft_eq(set(ntf["msg"]["indir"]), {1})
|
| 171 |
|
| 172 | reset.exec()
|
| 173 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 174 | if ntf is None:
|
| 175 | raise KsftFailEx("No notification received after reset")
|
| 176 | ksft_eq(ntf["name"], "rss-ntf")
|
| 177 | ksft_is(ntf["msg"].get("context"), None)
|
| 178 | ksft_ne(set(ntf["msg"]["indir"]), {1})
|
| 179 |
|
| 180 |
|
| 181 | def test_rxfh_indir_ctx_ntf(cfg):
|
| 182 | |
| 183 | Check that Netlink notifications are generated when RSS indirection
|
| 184 | table was modified on an additional RSS context.
|
| 185 |
|
| 186 | _require_2qs(cfg)
|
| 187 |
|
| 188 | ctx_id = _ethtool_create(cfg, "-X", "context new")
|
| 189 | defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
|
| 190 |
|
| 191 | ethnl = EthtoolFamily()
|
| 192 | ethnl.ntf_subscribe("monitor")
|
| 193 |
|
| 194 | ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1")
|
| 195 |
|
| 196 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 197 | if ntf is None:
|
| 198 | raise KsftFailEx("No notification received")
|
| 199 | ksft_eq(ntf["name"], "rss-ntf")
|
| 200 | ksft_eq(ntf["msg"].get("context"), ctx_id)
|
| 201 | ksft_eq(set(ntf["msg"]["indir"]), {1})
|
| 202 |
|
| 203 |
|
| 204 | def test_rxfh_nl_set_key(cfg):
|
| 205 | |
| 206 | Test setting hashing key via Netlink.
|
| 207 |
|
| 208 |
|
| 209 | dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 210 | defer(cfg.ethnl.rss_set,
|
| 211 | {"header": {"dev-index": cfg.ifindex},
|
| 212 | "hkey": dflt["hkey"], "indir": None})
|
| 213 |
|
| 214 | |
| 215 | with ksft_raises(NlError) as cm:
|
| 216 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 217 | "hkey": None})
|
| 218 | ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey')
|
| 219 |
|
| 220 | |
| 221 | mod = random.randbytes(len(dflt["hkey"]))
|
| 222 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 223 | "hkey": mod})
|
| 224 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 225 | ksft_eq(rss.get("hkey", [-1]), mod)
|
| 226 |
|
| 227 | |
| 228 | mod = random.randbytes(len(dflt["hkey"]))
|
| 229 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 230 | "indir": [0, 1], "hkey": mod})
|
| 231 | rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 232 | ksft_eq(rss.get("hkey", [-1]), mod)
|
| 233 | ksft_eq(set(rss.get("indir", [-1])), {0, 1})
|
| 234 |
|
| 235 |
|
| 236 | def test_rxfh_fields(cfg):
|
| 237 | |
| 238 | Test reading Rx Flow Hash over Netlink.
|
| 239 |
|
| 240 |
|
| 241 | flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
|
| 242 | ethnl = EthtoolFamily()
|
| 243 |
|
| 244 | cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 245 | for fl_type in flow_types:
|
| 246 | one = _ethtool_get_cfg(cfg, fl_type, to_nl=True)
|
| 247 | ksft_eq(one, cfg_nl["flow-hash"][fl_type],
|
| 248 | comment="Config for " + fl_type)
|
| 249 |
|
| 250 |
|
| 251 | def test_rxfh_fields_set(cfg):
|
| 252 |
|
| 253 |
|
| 254 | flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
|
| 255 |
|
| 256 | |
| 257 | cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 258 | |
| 259 | |
| 260 | changes = [{"flow-hash": cfg_old["flow-hash"],},
|
| 261 | {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
|
| 262 | if cfg_old.get("input-xfrm"):
|
| 263 | changes = list(reversed(changes))
|
| 264 | for old in changes:
|
| 265 | defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
|
| 266 |
|
| 267 | |
| 268 | if cfg_old.get("input-xfrm"):
|
| 269 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 270 | "input-xfrm": {}})
|
| 271 |
|
| 272 | for fl_type in flow_types:
|
| 273 | cur = _ethtool_get_cfg(cfg, fl_type)
|
| 274 | if cur == "sdfn":
|
| 275 | change_nl = {"ip-src", "ip-dst"}
|
| 276 | change_ic = "sd"
|
| 277 | else:
|
| 278 | change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
|
| 279 | change_ic = "sdfn"
|
| 280 |
|
| 281 | cfg.ethnl.rss_set({
|
| 282 | "header": {"dev-index": cfg.ifindex},
|
| 283 | "flow-hash": {fl_type: change_nl}
|
| 284 | })
|
| 285 | reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} "
|
| 286 | f"rx-flow-hash {fl_type} {cur}")
|
| 287 |
|
| 288 | cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 289 | ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type],
|
| 290 | comment=f"Config for {fl_type} over Netlink")
|
| 291 | cfg_ic = _ethtool_get_cfg(cfg, fl_type)
|
| 292 | ksft_eq(change_ic, cfg_ic,
|
| 293 | comment=f"Config for {fl_type} over IOCTL")
|
| 294 |
|
| 295 | reset.exec()
|
| 296 | cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 297 | ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type],
|
| 298 | comment=f"Un-config for {fl_type} over Netlink")
|
| 299 | cfg_ic = _ethtool_get_cfg(cfg, fl_type)
|
| 300 | ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL")
|
| 301 |
|
| 302 | |
| 303 | change = {"ip-src"}
|
| 304 | if change == cfg_old["flow-hash"]["tcp4"]:
|
| 305 | change = {"ip-dst"}
|
| 306 | cfg.ethnl.rss_set({
|
| 307 | "header": {"dev-index": cfg.ifindex},
|
| 308 | "flow-hash": {x: change for x in flow_types}
|
| 309 | })
|
| 310 |
|
| 311 | cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 312 | for fl_type in flow_types:
|
| 313 | ksft_eq(change, cfg_nl["flow-hash"][fl_type],
|
| 314 | comment=f"multi-config for {fl_type} over Netlink")
|
| 315 |
|
| 316 |
|
| 317 | def test_rxfh_fields_set_xfrm(cfg):
|
| 318 |
|
| 319 |
|
| 320 | def set_rss(cfg, xfrm, fh):
|
| 321 | cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
|
| 322 | "input-xfrm": xfrm, "flow-hash": fh})
|
| 323 |
|
| 324 | |
| 325 | cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
| 326 | |
| 327 | |
| 328 | changes = [{"flow-hash": cfg_old["flow-hash"],},
|
| 329 | {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
|
| 330 | if cfg_old.get("input-xfrm"):
|
| 331 | changes = list(reversed(changes))
|
| 332 | for old in changes:
|
| 333 | defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
|
| 334 |
|
| 335 | |
| 336 | set_rss(cfg, {}, {})
|
| 337 | set_rss(cfg, {}, {"tcp4": {"ip-src"}})
|
| 338 |
|
| 339 | |
| 340 | with ksft_raises(NlError):
|
| 341 | set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}})
|
| 342 | |
| 343 | set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}})
|
| 344 | no_support = False
|
| 345 | try:
|
| 346 | set_rss(cfg, {"sym-xor"}, {})
|
| 347 | except NlError:
|
| 348 | try:
|
| 349 | set_rss(cfg, {"sym-or-xor"}, {})
|
| 350 | except NlError:
|
| 351 | no_support = True
|
| 352 | if no_support:
|
| 353 | raise KsftSkipEx("no input-xfrm supported")
|
| 354 | |
| 355 | with ksft_raises(NlError):
|
| 356 | set_rss(cfg, {}, {"tcp4": {"ip-src"}})
|
| 357 |
|
| 358 |
|
| 359 | def test_rxfh_fields_ntf(cfg):
|
| 360 |
|
| 361 |
|
| 362 | cur = _ethtool_get_cfg(cfg, "tcp4")
|
| 363 | if cur == "sdfn":
|
| 364 | change = {"ip-src", "ip-dst"}
|
| 365 | else:
|
| 366 | change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
|
| 367 |
|
| 368 | ethnl = EthtoolFamily()
|
| 369 | ethnl.ntf_subscribe("monitor")
|
| 370 |
|
| 371 | ethnl.rss_set({
|
| 372 | "header": {"dev-index": cfg.ifindex},
|
| 373 | "flow-hash": {"tcp4": change}
|
| 374 | })
|
| 375 | reset = defer(ethtool,
|
| 376 | f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}")
|
| 377 |
|
| 378 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 379 | if ntf is None:
|
| 380 | raise KsftFailEx("No notification received after IOCTL change")
|
| 381 | ksft_eq(ntf["name"], "rss-ntf")
|
| 382 | ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change)
|
| 383 | ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
|
| 384 |
|
| 385 | reset.exec()
|
| 386 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 387 | if ntf is None:
|
| 388 | raise KsftFailEx("No notification received after Netlink change")
|
| 389 | ksft_eq(ntf["name"], "rss-ntf")
|
| 390 | ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change)
|
| 391 | ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
|
| 392 |
|
| 393 |
|
| 394 | def test_rss_ctx_add(cfg):
|
| 395 |
|
| 396 |
|
| 397 | _require_2qs(cfg)
|
| 398 |
|
| 399 | |
| 400 | ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
|
| 401 | d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete")
|
| 402 | ksft_ne(ctx.get("context", 0), 0)
|
| 403 | ksft_ne(set(ctx.get("indir", [0])), {0},
|
| 404 | comment="Driver should init the indirection table")
|
| 405 |
|
| 406 | |
| 407 | with ksft_raises(NlError) as cm:
|
| 408 | ctx = cfg.ethnl.rss_create_act({
|
| 409 | "header": {"dev-index": cfg.ifindex},
|
| 410 | "context": ctx.get("context"),
|
| 411 | })
|
| 412 | ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
|
| 413 | d.exec()
|
| 414 | ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY)
|
| 415 |
|
| 416 | |
| 417 | ctx_id = ctx.get("context")
|
| 418 | ctx = cfg.ethnl.rss_create_act({
|
| 419 | "header": {"dev-index": cfg.ifindex},
|
| 420 | "context": ctx_id,
|
| 421 | "indir": [1],
|
| 422 | })
|
| 423 | ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
|
| 424 | ksft_eq(ctx.get("context"), ctx_id)
|
| 425 | ksft_eq(set(ctx.get("indir", [0])), {1})
|
| 426 |
|
| 427 |
|
| 428 | def test_rss_ctx_ntf(cfg):
|
| 429 |
|
| 430 |
|
| 431 | ethnl = EthtoolFamily()
|
| 432 | ethnl.ntf_subscribe("monitor")
|
| 433 |
|
| 434 | |
| 435 | ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
|
| 436 | cfg.ethnl.rss_delete_act({
|
| 437 | "header": {"dev-index": cfg.ifindex},
|
| 438 | "context": ctx["context"],
|
| 439 | })
|
| 440 |
|
| 441 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 442 | if ntf is None:
|
| 443 | raise KsftFailEx("[NL] No notification after context creation")
|
| 444 | ksft_eq(ntf["name"], "rss-create-ntf")
|
| 445 | ksft_eq(ctx, ntf["msg"])
|
| 446 |
|
| 447 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 448 | if ntf is None:
|
| 449 | raise KsftFailEx("[NL] No notification after context deletion")
|
| 450 | ksft_eq(ntf["name"], "rss-delete-ntf")
|
| 451 |
|
| 452 | |
| 453 | ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new")
|
| 454 | ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete")
|
| 455 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 456 | if ntf is None:
|
| 457 | raise KsftFailEx("[IOCTL] No notification after context creation")
|
| 458 | ksft_eq(ntf["name"], "rss-create-ntf")
|
| 459 |
|
| 460 | ntf = next(ethnl.poll_ntf(duration=0.2), None)
|
| 461 | if ntf is None:
|
| 462 | raise KsftFailEx("[IOCTL] No notification after context deletion")
|
| 463 | ksft_eq(ntf["name"], "rss-delete-ntf")
|
| 464 |
|
| 465 |
|
| 466 | def main() -> None:
|
| 467 |
|
| 468 |
|
| 469 | with NetDrvEnv(__file__, nsim_test=False) as cfg:
|
| 470 | cfg.ethnl = EthtoolFamily()
|
| 471 | ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
|
| 472 | ksft_exit()
|
| 473 |
|
| 474 |
|
| 475 | if __name__ == "__main__":
|
| 476 | main()
|