Interoperability Testing
This example provides a standalone console script for testing libp2p ping functionality without Docker or Redis dependencies. It supports both listener and dialer roles and measures ping RTT and handshake times.
Usage
Run as listener (waits for connection):
$ python -m examples.interop.local_ping_test --listener --port 8000
Listener ready, listening on:
/ip4/127.0.0.1/tcp/8000/p2p/Qm...
Waiting for dialer to connect...
Run as dialer (connects to listener):
$ python -m examples.interop.local_ping_test --dialer --destination /ip4/127.0.0.1/tcp/8000/p2p/Qm...
Connecting to listener at: /ip4/127.0.0.1/tcp/8000/p2p/Qm...
Connected successfully
Performing ping test
{"handshakePlusOneRTTMillis": 15.2, "pingRTTMilllis": 2.1}
Options
--listener: Run as listener (wait for connection)--dialer: Run as dialer (connect to listener)--destination ADDR: Destination multiaddr (required for dialer)--port PORT: Port number (0 = auto-select)--transport {tcp,ws,quic-v1}: Transport protocol (default: tcp)--muxer {mplex,yamux}: Stream muxer (default: mplex)--security {noise,plaintext}: Security protocol (default: noise)--test-timeout SECONDS: Test timeout in seconds (default: 180)--debug: Enable debug logging
The full source code for this example is below:
1#!/usr/bin/env python3
2"""
3Local libp2p ping test implementation.
4
5This is a standalone console script version of the transport-interop ping test
6that runs without Docker or Redis dependencies. It supports both listener and
7dialer roles and measures ping RTT and handshake times.
8
9Usage:
10 # Run as listener (waits for connection)
11 python local_ping_test.py --listener --port 8000
12
13 # Run as dialer (connects to listener)
14 python local_ping_test.py --dialer --destination /ip4/127.0.0.1/tcp/8000/p2p/Qm...
15"""
16
17import argparse
18import json
19import logging
20import sys
21import time
22
23import multiaddr
24import trio
25
26from libp2p import create_mplex_muxer_option, create_yamux_muxer_option, new_host
27from libp2p.crypto.ed25519 import create_new_key_pair
28from libp2p.crypto.x25519 import create_new_key_pair as create_new_x25519_key_pair
29from libp2p.custom_types import TProtocol
30from libp2p.network.stream.net_stream import INetStream
31from libp2p.peer.peerinfo import info_from_p2p_addr
32from libp2p.security.insecure.transport import PLAINTEXT_PROTOCOL_ID, InsecureTransport
33from libp2p.security.noise.transport import (
34 PROTOCOL_ID as NOISE_PROTOCOL_ID,
35 Transport as NoiseTransport,
36)
37from libp2p.utils.address_validation import get_available_interfaces
38from libp2p.utils.multiaddr_utils import extract_ip_from_multiaddr
39
40PING_PROTOCOL_ID = TProtocol("/ipfs/ping/1.0.0")
41PING_LENGTH = 32
42MAX_TEST_TIMEOUT = 300
43DEFAULT_RESP_TIMEOUT = 30
44
45logger = logging.getLogger("libp2p.ping_test")
46
47
48def configure_logging(debug: bool = False):
49 """Configure logging based on debug flag."""
50 # Set up basic handler on root logger if not already configured
51 root_logger = logging.getLogger()
52 if not root_logger.handlers:
53 handler = logging.StreamHandler(sys.stderr)
54 formatter = logging.Formatter(
55 "%(asctime)s [%(levelname)s] [%(name)s] %(message)s"
56 )
57 handler.setFormatter(formatter)
58 root_logger.addHandler(handler)
59 root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
60
61 if debug:
62 # Set DEBUG level for all relevant loggers (they will propagate to root)
63 logger_names = [
64 "libp2p.ping_test",
65 "libp2p",
66 "libp2p.transport",
67 "libp2p.transport.quic",
68 "libp2p.transport.quic.connection",
69 "libp2p.transport.quic.listener",
70 "libp2p.network",
71 "libp2p.network.connection",
72 "libp2p.network.connection.swarm_connection",
73 "libp2p.protocol_muxer",
74 "libp2p.protocol_muxer.multiselect",
75 "libp2p.host",
76 "libp2p.host.basic_host",
77 ]
78 for logger_name in logger_names:
79 logger = logging.getLogger(logger_name)
80 logger.setLevel(logging.DEBUG)
81 # Ensure propagation is enabled (default, but be explicit)
82 logger.propagate = True
83 print("Debug logging enabled", file=sys.stderr)
84 else:
85 root_logger.setLevel(logging.INFO)
86 logging.getLogger("libp2p.ping_test").setLevel(logging.INFO)
87 # Suppress verbose logs from dependencies
88 for logger_name in [
89 "multiaddr",
90 "multiaddr.transforms",
91 "multiaddr.codecs",
92 "libp2p",
93 "libp2p.transport",
94 ]:
95 logging.getLogger(logger_name).setLevel(logging.WARNING)
96
97
98class PingTest:
99 def __init__(
100 self,
101 transport: str = "tcp",
102 muxer: str = "mplex",
103 security: str = "noise",
104 port: int = 0,
105 destination: str | None = None,
106 test_timeout: int = 180,
107 debug: bool = False,
108 ):
109 """Initialize ping test with configuration."""
110 self.transport = transport
111 self.muxer = muxer
112 self.security = security
113 self.port = port
114 self.destination = destination
115 self.is_dialer = destination is not None
116
117 raw_timeout = int(test_timeout)
118 self.test_timeout_seconds = min(raw_timeout, MAX_TEST_TIMEOUT)
119 self.resp_timeout = max(
120 DEFAULT_RESP_TIMEOUT, int(self.test_timeout_seconds * 0.6)
121 )
122 self.debug = debug
123
124 self.host = None
125 self.ping_received = False
126
127 def validate_configuration(self) -> None:
128 """Validate configuration parameters."""
129 valid_transports = ["tcp", "ws", "quic-v1"]
130 valid_security = ["noise", "plaintext"]
131 valid_muxers = ["mplex", "yamux"]
132
133 if self.transport not in valid_transports:
134 raise ValueError(
135 f"Unsupported transport: {self.transport}. "
136 f"Supported: {valid_transports}"
137 )
138 if self.security not in valid_security:
139 raise ValueError(
140 f"Unsupported security: {self.security}. Supported: {valid_security}"
141 )
142 if self.muxer not in valid_muxers:
143 raise ValueError(
144 f"Unsupported muxer: {self.muxer}. Supported: {valid_muxers}"
145 )
146
147 def create_security_options(self):
148 """Create security options based on configuration."""
149 key_pair = create_new_key_pair()
150
151 if self.security == "noise":
152 noise_key_pair = create_new_x25519_key_pair()
153 transport = NoiseTransport(
154 libp2p_keypair=key_pair,
155 noise_privkey=noise_key_pair.private_key,
156 early_data=None,
157 )
158 return {NOISE_PROTOCOL_ID: transport}, key_pair
159 elif self.security == "plaintext":
160 transport = InsecureTransport(
161 local_key_pair=key_pair,
162 secure_bytes_provider=None,
163 peerstore=None,
164 )
165 return {PLAINTEXT_PROTOCOL_ID: transport}, key_pair
166 else:
167 raise ValueError(f"Unsupported security: {self.security}")
168
169 def create_muxer_options(self):
170 """Create muxer options based on configuration."""
171 if self.muxer == "yamux":
172 return create_yamux_muxer_option()
173 elif self.muxer == "mplex":
174 return create_mplex_muxer_option()
175 else:
176 raise ValueError(f"Unsupported muxer: {self.muxer}")
177
178 def _get_ip_value(self, addr: multiaddr.Multiaddr) -> str | None:
179 """Extract IP value from multiaddr (IPv4 or IPv6)."""
180 return extract_ip_from_multiaddr(addr)
181
182 def _get_protocol_names(self, addr) -> list:
183 """Get protocol names from multiaddr."""
184 return [p.name for p in addr.protocols()]
185
186 def _build_quic_addr(self, ip_value: str, port: int) -> multiaddr.Multiaddr:
187 """Build QUIC address from IP and port."""
188 is_ipv6 = ":" in ip_value
189 if is_ipv6:
190 base = multiaddr.Multiaddr(f"/ip6/{ip_value}/udp/{port}")
191 else:
192 base = multiaddr.Multiaddr(f"/ip4/{ip_value}/udp/{port}")
193 return base.encapsulate(multiaddr.Multiaddr("/quic-v1"))
194
195 def create_listen_addresses(self, port: int = 0) -> list:
196 """Create listen addresses based on transport type."""
197 base_addrs = get_available_interfaces(port, protocol="tcp")
198
199 if self.transport == "quic-v1":
200 # Convert TCP addresses to UDP/QUIC addresses
201 quic_addrs = []
202 for addr in base_addrs:
203 try:
204 ip_value = self._get_ip_value(addr)
205 tcp_port = addr.value_for_protocol("tcp") or port
206 if ip_value:
207 quic_addr = self._build_quic_addr(ip_value, tcp_port)
208 # Preserve /p2p component if present
209 if "p2p" in self._get_protocol_names(addr):
210 p2p_value = addr.value_for_protocol("p2p")
211 if p2p_value:
212 quic_addr = quic_addr.encapsulate(
213 multiaddr.Multiaddr(f"/p2p/{p2p_value}")
214 )
215 quic_addrs.append(quic_addr)
216 except Exception as e:
217 print(
218 f"Error converting address {addr} to QUIC: {e}",
219 file=sys.stderr,
220 )
221 if quic_addrs:
222 return quic_addrs
223 return [self._build_quic_addr("127.0.0.1", port)]
224
225 elif self.transport == "ws":
226 # Add /ws protocol to TCP addresses
227 ws_addrs = []
228 for addr in base_addrs:
229 try:
230 protocols = self._get_protocol_names(addr)
231 if "ws" in protocols or "wss" in protocols:
232 ws_addrs.append(addr)
233 else:
234 # Preserve /p2p component
235 p2p_value = None
236 if "p2p" in protocols:
237 p2p_value = addr.value_for_protocol("p2p")
238 if p2p_value:
239 addr = addr.decapsulate(
240 multiaddr.Multiaddr(f"/p2p/{p2p_value}")
241 )
242 ws_addr = addr.encapsulate(multiaddr.Multiaddr("/ws"))
243 if p2p_value:
244 ws_addr = ws_addr.encapsulate(
245 multiaddr.Multiaddr(f"/p2p/{p2p_value}")
246 )
247 ws_addrs.append(ws_addr)
248 except Exception as e:
249 print(
250 f"Error converting address {addr} to WebSocket: {e}",
251 file=sys.stderr,
252 )
253 if ws_addrs:
254 return ws_addrs
255 return [multiaddr.Multiaddr(f"/ip4/127.0.0.1/tcp/{port}/ws")]
256
257 return base_addrs
258
259 def _get_peer_id(self, stream: INetStream) -> str:
260 """Get peer ID from stream, suppressing warnings."""
261 import warnings
262
263 with warnings.catch_warnings():
264 warnings.simplefilter("ignore")
265 try:
266 return str(stream.muxed_conn.peer_id) # type: ignore
267 except (AttributeError, Exception):
268 return "unknown"
269
270 async def handle_ping(self, stream: INetStream) -> None:
271 """Handle incoming ping requests."""
272 try:
273 payload = await stream.read(PING_LENGTH)
274 if payload is not None:
275 peer_id = self._get_peer_id(stream)
276 print(f"received ping from {peer_id}", file=sys.stderr)
277 await stream.write(payload)
278 print(f"responded with pong to {peer_id}", file=sys.stderr)
279 self.ping_received = True
280 except Exception as e:
281 import traceback
282
283 error_msg = (
284 str(e) if e else "Unknown error (exception object is None or empty)"
285 )
286 error_type = type(e).__name__ if e else "UnknownException"
287 print(f"Error in ping handler: {error_type}: {error_msg}", file=sys.stderr)
288 if self.debug:
289 traceback.print_exc(file=sys.stderr)
290 try:
291 await stream.reset()
292 except Exception:
293 pass
294
295 def log_protocols(self) -> None:
296 """Log registered protocols for debugging."""
297 try:
298 protocols = self.host.get_mux().get_protocols() # type: ignore
299 protocols_str = [str(p) for p in protocols if p is not None]
300 print(f"Registered protocols: {protocols_str}", file=sys.stderr)
301 except Exception as e:
302 print(f"Error getting protocols: {e}", file=sys.stderr)
303
304 async def send_ping(self, stream: INetStream) -> float:
305 """Send ping and measure RTT."""
306 try:
307 payload = b"\x01" * PING_LENGTH
308 peer_id = self._get_peer_id(stream)
309 print(f"sending ping to {peer_id}", file=sys.stderr)
310
311 ping_start = time.time()
312 await stream.write(payload)
313
314 with trio.fail_after(self.resp_timeout):
315 response = await stream.read(PING_LENGTH)
316 ping_end = time.time()
317
318 if response == payload:
319 print(f"received pong from {peer_id}", file=sys.stderr)
320 return (ping_end - ping_start) * 1000
321 else:
322 raise Exception("Invalid ping response")
323 except Exception as e:
324 print(f"error occurred: {e}", file=sys.stderr)
325 raise
326
327 def _filter_addresses_by_transport(self, addresses: list) -> list:
328 """Filter addresses to match current transport type."""
329 filtered = []
330 for addr in addresses:
331 protocols = self._get_protocol_names(addr)
332 if self.transport == "ws" and ("ws" in protocols or "wss" in protocols):
333 filtered.append(addr)
334 elif self.transport == "quic-v1" and "quic-v1" in protocols:
335 filtered.append(addr)
336 elif self.transport == "tcp" and not any(
337 p in protocols for p in ["ws", "wss", "quic-v1"]
338 ):
339 filtered.append(addr)
340 return filtered if filtered else addresses
341
342 def _get_publishable_address(self, addresses: list) -> str:
343 """Get the best address to publish, preferring non-loopback."""
344 filtered = self._filter_addresses_by_transport(addresses)
345 if not filtered:
346 print(
347 f"Warning: No addresses matched transport {self.transport}, "
348 f"using all addresses",
349 file=sys.stderr,
350 )
351 filtered = addresses
352
353 # Prefer non-loopback addresses
354 for addr in filtered:
355 ip_value = self._get_ip_value(addr)
356 if ip_value and ip_value not in ["127.0.0.1", "0.0.0.0", "::1", "::"]:
357 return str(addr)
358
359 # Fallback: use first address (for localhost testing)
360 return str(filtered[0])
361
362 async def run_listener(self) -> None:
363 """Run the listener role."""
364 self.validate_configuration()
365
366 # Create security and muxer options
367 sec_opt, key_pair = self.create_security_options()
368 muxer_opt = self.create_muxer_options()
369 listen_addrs = self.create_listen_addresses(self.port)
370
371 self.host = new_host( # type: ignore
372 key_pair=key_pair,
373 sec_opt=sec_opt,
374 muxer_opt=muxer_opt,
375 listen_addrs=listen_addrs,
376 enable_quic=(self.transport == "quic-v1"),
377 )
378 self.host.set_stream_handler(PING_PROTOCOL_ID, self.handle_ping) # type: ignore
379 self.log_protocols()
380
381 async with self.host.run(listen_addrs=listen_addrs): # type: ignore
382 all_addrs = self.host.get_addrs() # type: ignore
383 if not all_addrs:
384 raise RuntimeError("No listen addresses available")
385
386 actual_addr = self._get_publishable_address(all_addrs)
387 print("Listener ready, listening on:", file=sys.stderr)
388 for addr in all_addrs:
389 print(f" {addr}", file=sys.stderr)
390 print("\nTo connect, use this address:", file=sys.stderr)
391 print(f" {actual_addr}", file=sys.stderr)
392 print("Waiting for dialer to connect...", file=sys.stderr)
393
394 wait_timeout = min(self.test_timeout_seconds, MAX_TEST_TIMEOUT)
395 check_interval = 0.5
396 elapsed = 0
397
398 while elapsed < wait_timeout:
399 if self.ping_received:
400 print(
401 "Ping received and responded, listener exiting",
402 file=sys.stderr,
403 )
404 return
405 await trio.sleep(float(check_interval)) # type: ignore
406 elapsed += check_interval
407
408 if not self.ping_received:
409 print(
410 f"Timeout: No ping received within {wait_timeout} seconds",
411 file=sys.stderr,
412 )
413 sys.exit(1)
414
415 def _debug_connection_state(self, network, peer_id) -> None:
416 """Debug connection state (only if debug logging enabled)."""
417 if not self.debug:
418 return
419 try:
420 if hasattr(network, "get_connections_to_peer"):
421 connections = network.get_connections_to_peer(peer_id)
422 elif hasattr(network, "connections"):
423 connections = [
424 c
425 for c in network.connections.values()
426 if c.get_peer_id() == peer_id
427 ]
428 else:
429 connections = []
430 print(
431 f"[DEBUG] Found {len(connections)} connections to peer {peer_id}",
432 file=sys.stderr,
433 )
434 for i, conn in enumerate(connections):
435 muxed = hasattr(conn, "get_muxer")
436 print(
437 f"[DEBUG] Connection {i}: {type(conn).__name__}, muxed: {muxed}",
438 file=sys.stderr,
439 )
440 if muxed:
441 try:
442 muxer_type = type(conn.get_muxer()).__name__
443 print(
444 f"[DEBUG] Connection {i} muxer: {muxer_type}",
445 file=sys.stderr,
446 )
447 except Exception as e:
448 print(
449 f"[DEBUG] Connection {i} muxer error: {e}",
450 file=sys.stderr,
451 )
452 except Exception as e:
453 print(f"[DEBUG] Error checking connections: {e}", file=sys.stderr)
454
455 async def _create_stream_with_retry(self, peer_id) -> INetStream:
456 """Create ping stream with retry mechanism for connection readiness."""
457 max_retries = 3
458 retry_delay = 0.5
459
460 print("Creating ping stream", file=sys.stderr)
461 if self.debug:
462 print(
463 f"[DEBUG] About to create stream for protocol {PING_PROTOCOL_ID}",
464 file=sys.stderr,
465 )
466
467 for attempt in range(max_retries):
468 try:
469 stream = await self.host.new_stream(peer_id, [PING_PROTOCOL_ID]) # type: ignore
470 print("Ping stream created successfully", file=sys.stderr)
471 return stream
472 except Exception as e:
473 if attempt < max_retries - 1:
474 if self.debug:
475 print(
476 f"[DEBUG] Stream creation attempt {attempt + 1} "
477 f"failed: {e}, retrying...",
478 file=sys.stderr,
479 )
480 await trio.sleep(retry_delay)
481 else:
482 if self.debug:
483 print(
484 f"[DEBUG] Stream creation failed after {max_retries} "
485 f"attempts: {e}",
486 file=sys.stderr,
487 )
488 raise
489 raise RuntimeError("Failed to create ping stream after retries")
490
491 async def run_dialer(self) -> None:
492 """Run the dialer role."""
493 print("Running as dialer", file=sys.stderr)
494
495 try:
496 self.validate_configuration()
497
498 if not self.destination:
499 raise ValueError("Destination address is required for dialer mode")
500
501 listener_addr = self.destination
502 print(f"Connecting to listener at: {listener_addr}", file=sys.stderr)
503
504 # Create security and muxer options
505 sec_opt, key_pair = self.create_security_options()
506 muxer_opt = self.create_muxer_options()
507
508 # WS dialer workaround: need listen addresses to register transport
509 # (py-libp2p limitation)
510 dialer_listen_addrs = (
511 self.create_listen_addresses(self.port)
512 if self.transport == "ws"
513 else None
514 )
515 if dialer_listen_addrs:
516 addrs_str = [str(addr) for addr in dialer_listen_addrs]
517 print(
518 f"Registering WS transport for dialer with addresses: {addrs_str}",
519 file=sys.stderr,
520 )
521
522 host_kwargs = {
523 "key_pair": key_pair,
524 "sec_opt": sec_opt,
525 "muxer_opt": muxer_opt,
526 "enable_quic": (self.transport == "quic-v1"),
527 }
528 if dialer_listen_addrs:
529 host_kwargs["listen_addrs"] = dialer_listen_addrs # type: ignore
530
531 self.host = new_host(**host_kwargs) # type: ignore
532
533 async with self.host.run(listen_addrs=dialer_listen_addrs or []): # type: ignore
534 handshake_start = time.time()
535 maddr = multiaddr.Multiaddr(listener_addr)
536 info = info_from_p2p_addr(maddr)
537
538 print(f"Connecting to {listener_addr}", file=sys.stderr)
539 if self.debug:
540 print(
541 f"[DEBUG] About to call host.connect() for {info.peer_id}",
542 file=sys.stderr,
543 )
544 await self.host.connect(info) # type: ignore
545 print("Connected successfully", file=sys.stderr)
546 if self.debug:
547 print(
548 "[DEBUG] host.connect() completed, checking connection state",
549 file=sys.stderr,
550 )
551
552 self._debug_connection_state(self.host.get_network(), info.peer_id) # type: ignore
553
554 # Brief delay to ensure connection is fully ready for stream creation
555 await trio.sleep(0.1)
556
557 # Retry stream creation to handle cases where connection needs more time
558 stream = await self._create_stream_with_retry(info.peer_id)
559
560 print("Performing ping test", file=sys.stderr)
561 ping_rtt = await self.send_ping(stream)
562 print(f"Ping test completed, RTT: {ping_rtt}ms", file=sys.stderr)
563
564 handshake_plus_one_rtt = (time.time() - handshake_start) * 1000
565 result = {
566 "handshakePlusOneRTTMillis": handshake_plus_one_rtt,
567 "pingRTTMilllis": ping_rtt,
568 }
569 print(f"Outputting results: {result}", file=sys.stderr)
570 print(json.dumps(result))
571
572 await stream.close()
573 print("Stream closed successfully", file=sys.stderr)
574
575 except Exception as e:
576 print(f"Dialer error: {e}", file=sys.stderr)
577 if self.debug:
578 import traceback
579
580 traceback.print_exc(file=sys.stderr)
581 sys.exit(1)
582
583 async def run(self) -> None:
584 """Main run method."""
585 try:
586 if self.is_dialer:
587 await self.run_dialer()
588 else:
589 await self.run_listener()
590
591 except Exception as e:
592 print(f"Error: {e}", file=sys.stderr)
593 if self.debug:
594 import traceback
595
596 traceback.print_exc(file=sys.stderr)
597 sys.exit(1)
598
599
600def main():
601 """Main entry point."""
602 parser = argparse.ArgumentParser(
603 description="Local libp2p ping test - standalone console script",
604 formatter_class=argparse.RawDescriptionHelpFormatter,
605 epilog="""
606Examples:
607 # Run as listener
608 python local_ping_test.py --listener --port 8000
609
610 # Run as dialer (connect to listener)
611 python local_ping_test.py --dialer --destination /ip4/127.0.0.1/tcp/8000/p2p/Qm...
612
613 # With custom transport/muxer/security
614 python local_ping_test.py --listener --transport ws --muxer yamux --security noise
615 """,
616 )
617
618 # Mode selection
619 mode_group = parser.add_mutually_exclusive_group(required=True)
620 mode_group.add_argument(
621 "--listener",
622 action="store_true",
623 help="Run as listener (wait for connection)",
624 )
625 mode_group.add_argument(
626 "--dialer", action="store_true", help="Run as dialer (connect to listener)"
627 )
628
629 # Connection options
630 parser.add_argument(
631 "-d",
632 "--destination",
633 type=str,
634 help="Destination multiaddr (required for dialer)",
635 )
636 parser.add_argument(
637 "-p", "--port", type=int, default=0, help="Port number (0 = auto-select)"
638 )
639
640 # Configuration options
641 parser.add_argument(
642 "--transport",
643 choices=["tcp", "ws", "quic-v1"],
644 default="tcp",
645 help="Transport protocol (default: tcp)",
646 )
647 parser.add_argument(
648 "--muxer",
649 choices=["mplex", "yamux"],
650 default="mplex",
651 help="Stream muxer (default: mplex)",
652 )
653 parser.add_argument(
654 "--security",
655 choices=["noise", "plaintext"],
656 default="noise",
657 help="Security protocol (default: noise)",
658 )
659
660 # Test options
661 parser.add_argument(
662 "--test-timeout",
663 type=int,
664 default=180,
665 help="Test timeout in seconds (default: 180)",
666 )
667 parser.add_argument("--debug", action="store_true", help="Enable debug logging")
668
669 args = parser.parse_args()
670
671 # Validate arguments
672 if args.dialer and not args.destination:
673 parser.error("--destination is required when running as dialer")
674
675 configure_logging(debug=args.debug)
676
677 ping_test = PingTest(
678 transport=args.transport,
679 muxer=args.muxer,
680 security=args.security,
681 port=args.port,
682 destination=args.destination,
683 test_timeout=args.test_timeout,
684 debug=args.debug,
685 )
686
687 try:
688 trio.run(ping_test.run)
689 except KeyboardInterrupt:
690 print("\nInterrupted by user", file=sys.stderr)
691 sys.exit(0)
692
693
694if __name__ == "__main__":
695 main()