pytest-fastcollect 0.5.1__cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
- pytest_fastcollect/__init__.py +12 -0
- pytest_fastcollect/cache.py +171 -0
- pytest_fastcollect/constants.py +89 -0
- pytest_fastcollect/daemon.py +881 -0
- pytest_fastcollect/daemon_client.py +581 -0
- pytest_fastcollect/filter.py +204 -0
- pytest_fastcollect/plugin.py +601 -0
- pytest_fastcollect/py.typed +0 -0
- pytest_fastcollect/pytest_fastcollect.cpython-311-aarch64-linux-gnu.so +0 -0
- pytest_fastcollect/socket_strategy.py +217 -0
- pytest_fastcollect-0.5.1.dist-info/METADATA +686 -0
- pytest_fastcollect-0.5.1.dist-info/RECORD +15 -0
- pytest_fastcollect-0.5.1.dist-info/WHEEL +5 -0
- pytest_fastcollect-0.5.1.dist-info/entry_points.txt +2 -0
- pytest_fastcollect-0.5.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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)
|