Skip to content

Commit bcd706c

Browse files
authored
Add new helper method to provide a list of N free ports (#21662)
* Add new helper method to provide a list of N free ports * Fix changelog number * Add test coverage
1 parent 71e176b commit bcd706c

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add helper method to get a list of N free ports

datadog_checks_dev/datadog_checks/dev/utils.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,35 @@ def mock_context_manager(obj=None):
114114
yield obj
115115

116116

117-
def find_free_port(ip):
117+
def __get_free_port_from_socket(_socket: socket.socket, ip: str) -> int:
118+
_socket.bind((ip, 0))
119+
_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
120+
return _socket.getsockname()[1]
121+
122+
123+
def find_free_port(ip: str) -> int:
118124
"""Return a port available for listening on the given `ip`."""
119125
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
120-
s.bind((ip, 0))
121-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
122-
return s.getsockname()[1]
126+
return __get_free_port_from_socket(s, ip)
127+
128+
129+
def find_free_ports(ip: str, count: int) -> list[int]:
130+
"""Return `count` ports available for listening on the given `ip`."""
131+
sockets: list[socket.socket] = []
132+
ports: list[int] = []
133+
134+
try:
135+
# Create and bind all sockets first to reserve the ports
136+
for _ in range(count):
137+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
138+
sockets.append(s)
139+
ports.append(__get_free_port_from_socket(s, ip))
140+
141+
return ports
142+
finally:
143+
# Close all sockets
144+
for s in sockets:
145+
s.close()
123146

124147

125148
def get_ip():
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import socket
5+
from contextlib import closing
6+
7+
import pytest
8+
9+
from datadog_checks.dev.utils import find_free_port, find_free_ports
10+
11+
12+
def assert_ports_valid(ip, ports):
13+
assert all(isinstance(p, int) for p in ports)
14+
assert all(0 < port <= 65535 for port in ports)
15+
16+
for port in ports:
17+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
18+
s.bind((ip, port))
19+
20+
21+
@pytest.mark.parametrize('ip', ['127.0.0.1', '0.0.0.0'], ids=['localhost', 'any_interface'])
22+
def test_find_free_port(ip):
23+
port = find_free_port(ip)
24+
25+
assert_ports_valid(ip, [port])
26+
27+
28+
@pytest.mark.parametrize(
29+
'ip,count',
30+
[
31+
('127.0.0.1', 1),
32+
('127.0.0.1', 5),
33+
('127.0.0.1', 10),
34+
('0.0.0.0', 3),
35+
],
36+
ids=['localhost_single', 'localhost_multiple', 'localhost_many', 'any_interface'],
37+
)
38+
def test_find_free_ports(ip, count):
39+
ports = find_free_ports(ip, count)
40+
41+
assert len(ports) == count
42+
assert len(ports) == len(set(ports))
43+
assert_ports_valid(ip, ports)
44+
45+
46+
def test_find_free_ports_empty():
47+
ports = find_free_ports('127.0.0.1', 0)
48+
assert ports == []

0 commit comments

Comments
 (0)