pytest-fastcollect 0.5.1__cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ """
2
+ Socket Strategy: Abstract socket communication for cross-platform support.
3
+
4
+ Provides clean abstractions for Unix domain sockets and TCP sockets,
5
+ enabling seamless cross-platform operation without polluting daemon code.
6
+ """
7
+
8
+ import os
9
+ import socket
10
+ from abc import ABC, abstractmethod
11
+ from typing import Tuple, Any
12
+ import logging
13
+
14
+ logger = logging.getLogger('pytest_fastcollect.socket_strategy')
15
+
16
+
17
+ class SocketStrategy(ABC):
18
+ """Abstract base class for socket communication strategies."""
19
+
20
+ @abstractmethod
21
+ def create_server_socket(self) -> socket.socket:
22
+ """Create and bind a server socket.
23
+
24
+ Returns:
25
+ Bound socket ready to listen
26
+ """
27
+ pass
28
+
29
+ @abstractmethod
30
+ def create_client_socket(self, timeout: float) -> socket.socket:
31
+ """Create a client socket and connect.
32
+
33
+ Args:
34
+ timeout: Connection timeout in seconds
35
+
36
+ Returns:
37
+ Connected socket
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def cleanup(self):
43
+ """Clean up any resources (files, sockets, etc.)."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def get_connection_info(self) -> str:
48
+ """Get human-readable connection information.
49
+
50
+ Returns:
51
+ String describing how to connect (e.g., path or host:port)
52
+ """
53
+ pass
54
+
55
+ @abstractmethod
56
+ def is_available(self) -> bool:
57
+ """Check if connection information is available for clients.
58
+
59
+ Returns:
60
+ True if clients can connect, False otherwise
61
+ """
62
+ pass
63
+
64
+
65
+ class UnixSocketStrategy(SocketStrategy):
66
+ """Unix domain socket strategy (Linux, macOS)."""
67
+
68
+ def __init__(self, socket_path: str):
69
+ """Initialize Unix socket strategy.
70
+
71
+ Args:
72
+ socket_path: Path to Unix domain socket file
73
+ """
74
+ self.socket_path = socket_path
75
+ self.socket = None
76
+ logger.info(f"Using Unix domain socket strategy: {socket_path}")
77
+
78
+ def create_server_socket(self) -> socket.socket:
79
+ """Create and bind Unix domain socket."""
80
+ self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
81
+ self.socket.bind(self.socket_path)
82
+
83
+ # Make socket accessible (read/write for owner and group only)
84
+ # Using 0o660 instead of 0o666 for better security
85
+ try:
86
+ os.chmod(self.socket_path, 0o660)
87
+ except Exception as e:
88
+ logger.warning(f"Failed to set socket permissions: {e}")
89
+
90
+ logger.info(f"Unix socket bound to {self.socket_path}")
91
+ return self.socket
92
+
93
+ def create_client_socket(self, timeout: float) -> socket.socket:
94
+ """Create and connect Unix domain socket client."""
95
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
96
+ sock.settimeout(timeout)
97
+ sock.connect(self.socket_path)
98
+ logger.debug(f"Connected to Unix socket at {self.socket_path}")
99
+ return sock
100
+
101
+ def cleanup(self):
102
+ """Remove Unix socket file."""
103
+ if os.path.exists(self.socket_path):
104
+ try:
105
+ os.remove(self.socket_path)
106
+ logger.debug(f"Removed socket file: {self.socket_path}")
107
+ except Exception as e:
108
+ logger.error(f"Error removing socket file: {e}")
109
+
110
+ def get_connection_info(self) -> str:
111
+ """Get socket path."""
112
+ return f"Unix socket: {self.socket_path}"
113
+
114
+ def is_available(self) -> bool:
115
+ """Check if socket file exists."""
116
+ return os.path.exists(self.socket_path)
117
+
118
+
119
+ class TcpSocketStrategy(SocketStrategy):
120
+ """TCP socket strategy (Windows, cross-platform fallback)."""
121
+
122
+ def __init__(self, base_path: str):
123
+ """Initialize TCP socket strategy.
124
+
125
+ Args:
126
+ base_path: Base path for storing port file
127
+ """
128
+ self.base_path = base_path
129
+ self.port_file = base_path + ".port"
130
+ self.port = None
131
+ self.socket = None
132
+ logger.info("Using TCP socket strategy (localhost)")
133
+
134
+ def create_server_socket(self) -> socket.socket:
135
+ """Create and bind TCP socket on localhost."""
136
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
137
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
138
+
139
+ # Bind to localhost with automatic port selection
140
+ self.socket.bind(('127.0.0.1', 0))
141
+ self.port = self.socket.getsockname()[1]
142
+
143
+ # Save port to file for client discovery
144
+ try:
145
+ with open(self.port_file, 'w') as f:
146
+ f.write(str(self.port))
147
+ logger.info(f"TCP socket bound to 127.0.0.1:{self.port}")
148
+ except Exception as e:
149
+ logger.error(f"Failed to write port file: {e}")
150
+ raise
151
+
152
+ return self.socket
153
+
154
+ def create_client_socket(self, timeout: float) -> socket.socket:
155
+ """Create and connect TCP socket client."""
156
+ # Read port from file if not already known
157
+ if self.port is None:
158
+ self._read_port()
159
+
160
+ if self.port is None:
161
+ raise ConnectionError("TCP port not available. Is daemon running?")
162
+
163
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164
+ sock.settimeout(timeout)
165
+ sock.connect(('127.0.0.1', self.port))
166
+ logger.debug(f"Connected to TCP socket at 127.0.0.1:{self.port}")
167
+ return sock
168
+
169
+ def _read_port(self):
170
+ """Read port number from port file."""
171
+ if os.path.exists(self.port_file):
172
+ try:
173
+ with open(self.port_file, 'r') as f:
174
+ self.port = int(f.read().strip())
175
+ logger.debug(f"Read port {self.port} from {self.port_file}")
176
+ except Exception as e:
177
+ logger.warning(f"Failed to read port file: {e}")
178
+
179
+ def cleanup(self):
180
+ """Remove port file."""
181
+ if os.path.exists(self.port_file):
182
+ try:
183
+ os.remove(self.port_file)
184
+ logger.debug(f"Removed port file: {self.port_file}")
185
+ except Exception as e:
186
+ logger.error(f"Error removing port file: {e}")
187
+
188
+ def get_connection_info(self) -> str:
189
+ """Get TCP connection info."""
190
+ if self.port is None:
191
+ self._read_port()
192
+ return f"TCP socket: 127.0.0.1:{self.port}" if self.port else "TCP socket: port unknown"
193
+
194
+ def is_available(self) -> bool:
195
+ """Check if port file exists and is readable."""
196
+ if self.port is not None:
197
+ return True
198
+ self._read_port()
199
+ return self.port is not None
200
+
201
+
202
+ def create_socket_strategy(socket_path: str) -> SocketStrategy:
203
+ """Factory function to create appropriate socket strategy.
204
+
205
+ Args:
206
+ socket_path: Base path for socket (file path or identifier)
207
+
208
+ Returns:
209
+ SocketStrategy instance (Unix or TCP based on platform)
210
+ """
211
+ # Check if Unix sockets are available
212
+ has_unix_sockets = hasattr(socket, 'AF_UNIX')
213
+
214
+ if has_unix_sockets:
215
+ return UnixSocketStrategy(socket_path)
216
+ else:
217
+ return TcpSocketStrategy(socket_path)