testit-python-commons 4.2.8__py3-none-any.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.
- api_client_syncstorage/__init__.py +27 -0
- api_client_syncstorage/api/__init__.py +3 -0
- api_client_syncstorage/api/completion_api.py +302 -0
- api_client_syncstorage/api/health_api.py +158 -0
- api_client_syncstorage/api/system_api.py +158 -0
- api_client_syncstorage/api/test_results_api.py +182 -0
- api_client_syncstorage/api/workers_api.py +308 -0
- api_client_syncstorage/api_client.py +898 -0
- api_client_syncstorage/apis/__init__.py +21 -0
- api_client_syncstorage/configuration.py +450 -0
- api_client_syncstorage/exceptions.py +158 -0
- api_client_syncstorage/model/__init__.py +5 -0
- api_client_syncstorage/model/completion_response.py +267 -0
- api_client_syncstorage/model/health_status_response.py +267 -0
- api_client_syncstorage/model/register_request.py +275 -0
- api_client_syncstorage/model/register_response.py +279 -0
- api_client_syncstorage/model/set_worker_status_request.py +271 -0
- api_client_syncstorage/model/set_worker_status_response.py +267 -0
- api_client_syncstorage/model/shutdown_response.py +267 -0
- api_client_syncstorage/model/test_result_cut_api_model.py +279 -0
- api_client_syncstorage/model/test_result_save_response.py +267 -0
- api_client_syncstorage/model_utils.py +2058 -0
- api_client_syncstorage/models/__init__.py +20 -0
- api_client_syncstorage/rest.py +356 -0
- testit.py +69 -0
- testit_python_commons/__init__.py +0 -0
- testit_python_commons/app_properties.py +257 -0
- testit_python_commons/client/__init__.py +9 -0
- testit_python_commons/client/api_client.py +560 -0
- testit_python_commons/client/client_configuration.py +74 -0
- testit_python_commons/client/converter.py +618 -0
- testit_python_commons/client/helpers/__init__.py +0 -0
- testit_python_commons/client/helpers/bulk_autotest_helper.py +246 -0
- testit_python_commons/client/helpers/threads_manager.py +86 -0
- testit_python_commons/client/models/__init__.py +8 -0
- testit_python_commons/client/models/threads_for_create_and_result.py +35 -0
- testit_python_commons/client/models/threads_for_update_and_result.py +45 -0
- testit_python_commons/configurations/__init__.py +0 -0
- testit_python_commons/configurations/properties_names.py +51 -0
- testit_python_commons/decorators.py +221 -0
- testit_python_commons/dynamic_methods.py +237 -0
- testit_python_commons/models/__init__.py +5 -0
- testit_python_commons/models/adapter_mode.py +4 -0
- testit_python_commons/models/fixture.py +24 -0
- testit_python_commons/models/link.py +53 -0
- testit_python_commons/models/link_type.py +10 -0
- testit_python_commons/models/outcome_type.py +5 -0
- testit_python_commons/models/status_type.py +6 -0
- testit_python_commons/models/step_result.py +90 -0
- testit_python_commons/models/test_result.py +297 -0
- testit_python_commons/models/test_result_with_all_fixture_step_results_model.py +28 -0
- testit_python_commons/services/__init__.py +41 -0
- testit_python_commons/services/adapter_manager.py +318 -0
- testit_python_commons/services/adapter_manager_configuration.py +42 -0
- testit_python_commons/services/fixture_manager.py +60 -0
- testit_python_commons/services/fixture_storage.py +40 -0
- testit_python_commons/services/logger.py +32 -0
- testit_python_commons/services/plugin_manager.py +101 -0
- testit_python_commons/services/retry.py +92 -0
- testit_python_commons/services/step_manager.py +36 -0
- testit_python_commons/services/step_result_storage.py +26 -0
- testit_python_commons/services/sync_storage/__init__.py +9 -0
- testit_python_commons/services/sync_storage/sync_storage_runner.py +589 -0
- testit_python_commons/services/utils.py +132 -0
- testit_python_commons/step.py +101 -0
- testit_python_commons/utils/__init__.py +3 -0
- testit_python_commons/utils/html_escape_utils.py +219 -0
- testit_python_commons-4.2.8.dist-info/METADATA +52 -0
- testit_python_commons-4.2.8.dist-info/RECORD +71 -0
- testit_python_commons-4.2.8.dist-info/WHEEL +5 -0
- testit_python_commons-4.2.8.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import urllib3
|
|
15
|
+
from api_client_syncstorage.api.test_results_api import TestResultsApi
|
|
16
|
+
from api_client_syncstorage.api.workers_api import WorkersApi
|
|
17
|
+
from api_client_syncstorage.api_client import ApiClient as SyncStorageApiClient
|
|
18
|
+
from api_client_syncstorage.configuration import (
|
|
19
|
+
Configuration as SyncStorageConfiguration,
|
|
20
|
+
)
|
|
21
|
+
from api_client_syncstorage.model.test_result_cut_api_model import (
|
|
22
|
+
TestResultCutApiModel,
|
|
23
|
+
)
|
|
24
|
+
from api_client_syncstorage.model.register_request import RegisterRequest
|
|
25
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
26
|
+
from api_client_syncstorage.model.set_worker_status_request import SetWorkerStatusRequest
|
|
27
|
+
from testit_python_commons.models.test_result import TestResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Disable SSL warnings
|
|
31
|
+
urllib3.disable_warnings(InsecureRequestWarning)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SyncStorageRunner:
|
|
37
|
+
"""
|
|
38
|
+
Python implementation of the SyncStorageRunner following the Java adapter specification.
|
|
39
|
+
Manages interaction with the Sync Storage service for coordinating test execution
|
|
40
|
+
across multiple workers.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
SYNC_STORAGE_VERSION = "v0.3.7-tms-5.7"
|
|
44
|
+
SYNC_STORAGE_REPO_URL = (
|
|
45
|
+
"https://github.com/testit-tms/sync-storage-public/releases/download/"
|
|
46
|
+
)
|
|
47
|
+
AMD64 = "amd64"
|
|
48
|
+
ARM64 = "arm64"
|
|
49
|
+
SYNC_STORAGE_STARTUP_TIMEOUT = 5 # seconds
|
|
50
|
+
KEEP_ALIVE_INTERVAL_SECONDS = 30
|
|
51
|
+
KEEP_ALIVE_REQUEST_TIMEOUT_SECONDS = 5
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
test_run_id: str,
|
|
56
|
+
port: Optional[str] = None,
|
|
57
|
+
base_url: Optional[str] = None,
|
|
58
|
+
private_token: Optional[str] = None,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Initialize the SyncStorageRunner.
|
|
62
|
+
|
|
63
|
+
:param test_run_id: The ID of the test run
|
|
64
|
+
:param port: Port for Sync Storage service (default: 49152)
|
|
65
|
+
:param base_url: Test IT server URL
|
|
66
|
+
:param private_token: Authentication token for Test IT API
|
|
67
|
+
"""
|
|
68
|
+
self.test_run_id = test_run_id
|
|
69
|
+
self.port = port
|
|
70
|
+
self.base_url = base_url
|
|
71
|
+
self.private_token = private_token
|
|
72
|
+
|
|
73
|
+
# Worker identification
|
|
74
|
+
self.worker_pid: str = f"worker-{threading.get_ident()}-{int(time.time())}"
|
|
75
|
+
self.is_master: bool = False
|
|
76
|
+
self.is_already_in_progress: bool = False
|
|
77
|
+
self.is_running: bool = False
|
|
78
|
+
self.is_external: bool = False
|
|
79
|
+
|
|
80
|
+
# Sync Storage process management
|
|
81
|
+
self.sync_storage_process: Optional[subprocess.Popen] = None
|
|
82
|
+
self.sync_storage_executable_path: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
# API clients
|
|
85
|
+
self.workers_api: Optional[WorkersApi] = None
|
|
86
|
+
self.test_results_api: Optional[TestResultsApi] = None
|
|
87
|
+
|
|
88
|
+
self._keep_alive_stop_event = threading.Event()
|
|
89
|
+
self._keep_alive_thread: Optional[threading.Thread] = None
|
|
90
|
+
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"Initialized SyncStorageRunner with test_run_id={test_run_id}, port={self.port}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def start(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Start the Sync Storage service and register the worker.
|
|
98
|
+
|
|
99
|
+
:return: True if successfully started and registered, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
logger.info("Starting Sync Storage service!")
|
|
102
|
+
try:
|
|
103
|
+
if self.is_running:
|
|
104
|
+
logger.info("SyncStorage already running")
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
logger.info("SyncStorage is not running")
|
|
108
|
+
|
|
109
|
+
# Check if Sync Storage is already running
|
|
110
|
+
if self._is_sync_storage_already_running():
|
|
111
|
+
logger.info(
|
|
112
|
+
f"SyncStorage already started {self.port}. Connecting to existing one..."
|
|
113
|
+
)
|
|
114
|
+
self.is_running = True
|
|
115
|
+
self.is_external = True
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
self._register_worker_with_retry()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Error registering worker: {e}")
|
|
121
|
+
|
|
122
|
+
self._start_keep_alive()
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
# Get executable file name for current platform
|
|
126
|
+
executable_filename = self._get_file_name_by_arch_and_os()
|
|
127
|
+
|
|
128
|
+
# Prepare command to start Sync Storage
|
|
129
|
+
command = [executable_filename]
|
|
130
|
+
|
|
131
|
+
if self.test_run_id:
|
|
132
|
+
command.extend(["--testRunId", self.test_run_id])
|
|
133
|
+
|
|
134
|
+
if self.port:
|
|
135
|
+
command.extend(["--port", self.port])
|
|
136
|
+
|
|
137
|
+
if self.base_url:
|
|
138
|
+
command.extend(["--baseURL", self.base_url])
|
|
139
|
+
|
|
140
|
+
if self.private_token:
|
|
141
|
+
command.extend(["--privateToken", self.private_token])
|
|
142
|
+
|
|
143
|
+
# Prepare executable file (download if needed)
|
|
144
|
+
prepared_executable_path = self._prepare_executable_file(
|
|
145
|
+
executable_filename
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Update command with prepared path
|
|
149
|
+
command[0] = prepared_executable_path
|
|
150
|
+
|
|
151
|
+
logger.info(f"Starting SyncStorage with command: {' '.join(command)}")
|
|
152
|
+
|
|
153
|
+
# Start process
|
|
154
|
+
process_builder = subprocess.Popen(
|
|
155
|
+
command,
|
|
156
|
+
cwd=os.path.dirname(prepared_executable_path),
|
|
157
|
+
stdout=subprocess.PIPE,
|
|
158
|
+
stderr=subprocess.STDOUT,
|
|
159
|
+
text=True,
|
|
160
|
+
bufsize=1,
|
|
161
|
+
universal_newlines=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.sync_storage_process = process_builder
|
|
165
|
+
|
|
166
|
+
# Start output reader in background thread
|
|
167
|
+
self._start_output_reader()
|
|
168
|
+
|
|
169
|
+
# Wait for server startup
|
|
170
|
+
if self._wait_for_server_startup(self.SYNC_STORAGE_STARTUP_TIMEOUT):
|
|
171
|
+
self.is_running = True
|
|
172
|
+
logger.info(f"SyncStorage started successfully on port {self.port}")
|
|
173
|
+
time.sleep(2) # Wait a bit more as in Java implementation
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
self._register_worker_with_retry()
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Error registering worker: {e}")
|
|
179
|
+
|
|
180
|
+
self._start_keep_alive()
|
|
181
|
+
return True
|
|
182
|
+
else:
|
|
183
|
+
raise RuntimeError("Cannot start the SyncStorage until timeout")
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error starting Sync Storage: {e}", exc_info=True)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def get_worker_pid(self) -> str:
|
|
190
|
+
"""Get the worker PID."""
|
|
191
|
+
return self.worker_pid
|
|
192
|
+
|
|
193
|
+
def get_test_run_id(self) -> str:
|
|
194
|
+
"""Get the test run ID."""
|
|
195
|
+
return self.test_run_id
|
|
196
|
+
|
|
197
|
+
def is_master_worker(self) -> bool:
|
|
198
|
+
"""Check if this is the master worker."""
|
|
199
|
+
return self.is_master
|
|
200
|
+
|
|
201
|
+
def is_already_in_progress_flag(self) -> bool:
|
|
202
|
+
"""Check if already in progress flag is set."""
|
|
203
|
+
return self.is_already_in_progress
|
|
204
|
+
|
|
205
|
+
def set_is_already_in_progress(self, value: bool):
|
|
206
|
+
"""Set the already in progress flag."""
|
|
207
|
+
self.is_already_in_progress = value
|
|
208
|
+
|
|
209
|
+
def is_running_status(self) -> bool:
|
|
210
|
+
"""Check if Sync Storage is running."""
|
|
211
|
+
return self.is_running
|
|
212
|
+
|
|
213
|
+
def is_running_as_process(self) -> bool:
|
|
214
|
+
"""Check if Sync Storage is running as a process."""
|
|
215
|
+
return (
|
|
216
|
+
self.sync_storage_process is not None
|
|
217
|
+
and self.sync_storage_process.poll() is None
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def get_url(self) -> str:
|
|
221
|
+
"""Get the Sync Storage URL."""
|
|
222
|
+
return f"http://localhost:{self.port}"
|
|
223
|
+
|
|
224
|
+
def _keep_alive(self) -> None:
|
|
225
|
+
try:
|
|
226
|
+
payload = json.dumps({
|
|
227
|
+
"pid": self.worker_pid,
|
|
228
|
+
"testRunId": self.test_run_id,
|
|
229
|
+
}).encode("utf-8")
|
|
230
|
+
request = urllib.request.Request(
|
|
231
|
+
f"{self.get_url()}/keep_alive",
|
|
232
|
+
data=payload,
|
|
233
|
+
headers={"Content-Type": "application/json"},
|
|
234
|
+
method="POST",
|
|
235
|
+
)
|
|
236
|
+
urllib.request.urlopen(
|
|
237
|
+
request,
|
|
238
|
+
timeout=self.KEEP_ALIVE_REQUEST_TIMEOUT_SECONDS,
|
|
239
|
+
)
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
def _keep_alive_loop(self) -> None:
|
|
244
|
+
while not self._keep_alive_stop_event.is_set():
|
|
245
|
+
self._keep_alive()
|
|
246
|
+
if self._keep_alive_stop_event.wait(self.KEEP_ALIVE_INTERVAL_SECONDS):
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
def _start_keep_alive(self) -> None:
|
|
250
|
+
if self._keep_alive_thread and self._keep_alive_thread.is_alive():
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
self._keep_alive_stop_event.clear()
|
|
254
|
+
self._keep_alive_thread = threading.Thread(
|
|
255
|
+
target=self._keep_alive_loop,
|
|
256
|
+
name="sync-storage-keep-alive",
|
|
257
|
+
daemon=True,
|
|
258
|
+
)
|
|
259
|
+
self._keep_alive_thread.start()
|
|
260
|
+
logger.debug("Sync Storage keep-alive thread started")
|
|
261
|
+
|
|
262
|
+
def _stop_keep_alive(self) -> None:
|
|
263
|
+
self._keep_alive_stop_event.set()
|
|
264
|
+
if self._keep_alive_thread and self._keep_alive_thread.is_alive():
|
|
265
|
+
self._keep_alive_thread.join(timeout=1)
|
|
266
|
+
|
|
267
|
+
def send_in_progress_test_result(
|
|
268
|
+
self, model: TestResultCutApiModel
|
|
269
|
+
) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Send test result to Sync Storage if this worker is the master.
|
|
272
|
+
|
|
273
|
+
:param model: TestResultCutApiModel instance
|
|
274
|
+
:return: True if successfully sent, False otherwise
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
# Only master worker sends test results
|
|
278
|
+
if not self.is_master:
|
|
279
|
+
logger.debug(
|
|
280
|
+
"Not master worker, skipping sending test result to Sync Storage"
|
|
281
|
+
)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
# Prevent duplicate sending
|
|
285
|
+
if self.is_already_in_progress:
|
|
286
|
+
logger.debug("Test already in progress, skipping duplicate send")
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
logger.debug("Sending in-progress test result to Sync Storage")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# Send to Sync Storage using API client
|
|
293
|
+
if self.test_results_api is not None:
|
|
294
|
+
response = self.test_results_api.in_progress_test_result_post(
|
|
295
|
+
self.test_run_id,
|
|
296
|
+
model, # test_result_cut_api_model
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Mark as in progress to prevent duplicates
|
|
300
|
+
self.is_already_in_progress = True
|
|
301
|
+
|
|
302
|
+
logger.debug(
|
|
303
|
+
f"Successfully sent test result to Sync Storage: {response}"
|
|
304
|
+
)
|
|
305
|
+
return True
|
|
306
|
+
else:
|
|
307
|
+
logger.error("TestResultsApi not initialized")
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.warning(
|
|
312
|
+
f"Failed to send test result to Sync Storage: {e}", exc_info=True
|
|
313
|
+
)
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def _is_sync_storage_already_running(self) -> bool:
|
|
317
|
+
"""
|
|
318
|
+
Check if Sync Storage is already running on the specified port.
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
:return: True if running, False otherwise
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
url = f"http://localhost:{self.port}/health"
|
|
327
|
+
req = urllib.request.Request(url)
|
|
328
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
329
|
+
return response.getcode() == 200
|
|
330
|
+
except Exception:
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
def _register_worker_with_retry(self):
|
|
334
|
+
"""Register worker with retry logic."""
|
|
335
|
+
self._register_worker()
|
|
336
|
+
|
|
337
|
+
def _start_output_reader(self):
|
|
338
|
+
"""Start reading output from Sync Storage process in a background thread."""
|
|
339
|
+
|
|
340
|
+
def read_output():
|
|
341
|
+
try:
|
|
342
|
+
if self.sync_storage_process and self.sync_storage_process.stdout:
|
|
343
|
+
# Read output with explicit encoding (e.g., 'utf-8' or 'latin-1')
|
|
344
|
+
for line in iter(self.sync_storage_process.stdout.readline, ""):
|
|
345
|
+
if line:
|
|
346
|
+
# Decode the line explicitly to avoid encoding issues
|
|
347
|
+
logger.info(f"SyncStorage: {line.rstrip()}")
|
|
348
|
+
self.sync_storage_process.stdout.close()
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.warning(f"Error reading SyncStorage output: {e}")
|
|
351
|
+
|
|
352
|
+
output_thread = threading.Thread(target=read_output, daemon=True)
|
|
353
|
+
output_thread.start()
|
|
354
|
+
|
|
355
|
+
def _wait_for_server_startup(self, timeout_seconds: int) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Wait for Sync Storage server to start up.
|
|
358
|
+
|
|
359
|
+
:param timeout_seconds: Timeout in seconds
|
|
360
|
+
:return: True if server started, False otherwise
|
|
361
|
+
"""
|
|
362
|
+
start_time = time.time()
|
|
363
|
+
while time.time() - start_time < timeout_seconds:
|
|
364
|
+
if self._is_sync_storage_already_running():
|
|
365
|
+
return True
|
|
366
|
+
time.sleep(1)
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def _prepare_executable_file(self, original_executable_path: str) -> str:
|
|
370
|
+
"""
|
|
371
|
+
Prepare executable file by downloading if needed.
|
|
372
|
+
|
|
373
|
+
:param original_executable_path: Original executable path
|
|
374
|
+
:return: Path to prepared executable
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
current_dir = os.getcwd()
|
|
378
|
+
caches_dir = os.path.join(current_dir, "build", ".caches")
|
|
379
|
+
|
|
380
|
+
# Create caches directory if it doesn't exist
|
|
381
|
+
os.makedirs(caches_dir, exist_ok=True)
|
|
382
|
+
|
|
383
|
+
original_path = Path(original_executable_path)
|
|
384
|
+
file_name = original_path.name
|
|
385
|
+
target_path = os.path.join(caches_dir, file_name)
|
|
386
|
+
|
|
387
|
+
if os.path.exists(target_path):
|
|
388
|
+
logger.info(f"Using existing file: {target_path}")
|
|
389
|
+
|
|
390
|
+
# Make file executable for Unix-based systems
|
|
391
|
+
if not platform.system().lower().startswith("windows"):
|
|
392
|
+
os.chmod(target_path, 0o755)
|
|
393
|
+
|
|
394
|
+
return target_path
|
|
395
|
+
|
|
396
|
+
logger.info("File not present, downloading from GitHub Releases")
|
|
397
|
+
self._download_executable_from_github(target_path)
|
|
398
|
+
|
|
399
|
+
return target_path
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Error preparing executable file: {e}")
|
|
402
|
+
raise
|
|
403
|
+
|
|
404
|
+
def _download_executable_from_github(self, target_path: str):
|
|
405
|
+
"""
|
|
406
|
+
Download Sync Storage executable from GitHub.
|
|
407
|
+
|
|
408
|
+
:param target_path: Target path to save the executable
|
|
409
|
+
"""
|
|
410
|
+
try:
|
|
411
|
+
# Determine download URL
|
|
412
|
+
download_url = self._get_download_url_for_current_platform()
|
|
413
|
+
|
|
414
|
+
logger.info(f"Downloading file from: {download_url}")
|
|
415
|
+
logger.info(f"Saving in: {target_path}")
|
|
416
|
+
|
|
417
|
+
# Download file
|
|
418
|
+
req = urllib.request.Request(download_url)
|
|
419
|
+
req.add_header("User-Agent", "TestIT Python Adapter")
|
|
420
|
+
|
|
421
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
422
|
+
with open(target_path, "wb") as f:
|
|
423
|
+
while True:
|
|
424
|
+
chunk = response.read(8192)
|
|
425
|
+
if not chunk:
|
|
426
|
+
break
|
|
427
|
+
f.write(chunk)
|
|
428
|
+
|
|
429
|
+
logger.info(f"File downloaded successfully: {target_path}")
|
|
430
|
+
|
|
431
|
+
# Make file executable for Unix-based systems
|
|
432
|
+
if not platform.system().lower().startswith("windows"):
|
|
433
|
+
os.chmod(target_path, 0o755)
|
|
434
|
+
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"Error downloading file: {e}")
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
def _get_file_name_by_arch_and_os(self) -> str:
|
|
440
|
+
"""
|
|
441
|
+
Get executable file name based on OS and architecture.
|
|
442
|
+
|
|
443
|
+
:return: File name
|
|
444
|
+
"""
|
|
445
|
+
os_name = platform.system().lower()
|
|
446
|
+
os_arch = platform.machine().lower()
|
|
447
|
+
|
|
448
|
+
# Determine OS part. Check darwin first because "darwin" contains "win".
|
|
449
|
+
if "mac" in os_name or "darwin" in os_name:
|
|
450
|
+
os_part = "darwin"
|
|
451
|
+
elif os_name.startswith("win"):
|
|
452
|
+
os_part = "windows"
|
|
453
|
+
elif "linux" in os_name:
|
|
454
|
+
os_part = "linux"
|
|
455
|
+
else:
|
|
456
|
+
raise RuntimeError(f"Unsupported OS, please contact dev team: {os_name}")
|
|
457
|
+
|
|
458
|
+
return self._make_file_name(os_arch, os_part)
|
|
459
|
+
|
|
460
|
+
def _is_mac_os(self, os_name: str) -> bool:
|
|
461
|
+
"""Check if OS is macOS."""
|
|
462
|
+
return "mac" in os_name or "darwin" in os_name
|
|
463
|
+
|
|
464
|
+
def _is_linux(self, os_name: str) -> bool:
|
|
465
|
+
"""Check if OS is Linux."""
|
|
466
|
+
return "linux" in os_name
|
|
467
|
+
|
|
468
|
+
def _get_download_url_for_current_platform(self) -> str:
|
|
469
|
+
"""
|
|
470
|
+
Get download URL for current platform.
|
|
471
|
+
|
|
472
|
+
:return: Download URL
|
|
473
|
+
"""
|
|
474
|
+
file_name = self._get_file_name_by_arch_and_os()
|
|
475
|
+
return f"{self.SYNC_STORAGE_REPO_URL}{self.SYNC_STORAGE_VERSION}/{file_name}"
|
|
476
|
+
|
|
477
|
+
def _make_file_name(self, os_arch: str, os_part: str) -> str:
|
|
478
|
+
"""
|
|
479
|
+
Make file name based on OS architecture and platform.
|
|
480
|
+
|
|
481
|
+
:param os_arch: OS architecture
|
|
482
|
+
:param os_part: OS part
|
|
483
|
+
:return: File name
|
|
484
|
+
"""
|
|
485
|
+
# Determine architecture part
|
|
486
|
+
if self.AMD64 in os_arch or "x86_64" in os_arch:
|
|
487
|
+
arch_part = self.AMD64
|
|
488
|
+
elif self.ARM64 in os_arch or "aarch64" in os_arch:
|
|
489
|
+
arch_part = self.ARM64
|
|
490
|
+
else:
|
|
491
|
+
raise RuntimeError(f"Unsupported architecture: {os_arch}")
|
|
492
|
+
|
|
493
|
+
# Form file name
|
|
494
|
+
file_name = f"syncstorage-{self.SYNC_STORAGE_VERSION}-{os_part}_{arch_part}"
|
|
495
|
+
if os_part == "windows":
|
|
496
|
+
file_name += ".exe"
|
|
497
|
+
return file_name
|
|
498
|
+
|
|
499
|
+
def _initialize_api_clients(self):
|
|
500
|
+
"""
|
|
501
|
+
Initialize the Sync Storage API clients.
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
# Configure API client
|
|
505
|
+
config = SyncStorageConfiguration()
|
|
506
|
+
config.host = f"http://localhost:{self.port}"
|
|
507
|
+
config.verify_ssl = False # Disable SSL verification for localhost
|
|
508
|
+
|
|
509
|
+
api_client = SyncStorageApiClient(configuration=config)
|
|
510
|
+
|
|
511
|
+
# Initialize APIs
|
|
512
|
+
self.workers_api = WorkersApi(api_client=api_client)
|
|
513
|
+
self.test_results_api = TestResultsApi(api_client=api_client)
|
|
514
|
+
|
|
515
|
+
logger.debug("Successfully initialized Sync Storage API clients")
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(f"Error initializing API clients: {e}", exc_info=True)
|
|
519
|
+
raise
|
|
520
|
+
|
|
521
|
+
def _register_worker(self) -> bool:
|
|
522
|
+
"""
|
|
523
|
+
Register the worker with the Sync Storage service.
|
|
524
|
+
|
|
525
|
+
:return: True if successfully registered, False otherwise
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
# Initialize API clients if not already done
|
|
529
|
+
|
|
530
|
+
if self.workers_api is None:
|
|
531
|
+
self._initialize_api_clients()
|
|
532
|
+
|
|
533
|
+
if self.workers_api is None:
|
|
534
|
+
logger.error("WorkersApi not initialized")
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
logger.info(
|
|
538
|
+
f"Registering worker {self.worker_pid} with test run {self.test_run_id}"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Create registration request
|
|
542
|
+
|
|
543
|
+
register_request = RegisterRequest(
|
|
544
|
+
pid=self.worker_pid, test_run_id=self.test_run_id, base_url=self.base_url, private_token=self.private_token
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Send registration request
|
|
548
|
+
|
|
549
|
+
response = self.workers_api.register_post(register_request=register_request)
|
|
550
|
+
|
|
551
|
+
# Check if this worker is the master
|
|
552
|
+
|
|
553
|
+
self.is_master = getattr(response, "is_master", False)
|
|
554
|
+
|
|
555
|
+
if self.is_master:
|
|
556
|
+
logger.info(f"Master worker registered, PID: {self.worker_pid}")
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
logger.info(f"Worker registered successfully, PID: {self.worker_pid}")
|
|
560
|
+
|
|
561
|
+
return True
|
|
562
|
+
|
|
563
|
+
except Exception as e:
|
|
564
|
+
logger.error(f"Error registering worker: {e}", exc_info=True)
|
|
565
|
+
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
def set_worker_status(self, status: str):
|
|
569
|
+
if self.workers_api is None:
|
|
570
|
+
self._initialize_api_clients()
|
|
571
|
+
try:
|
|
572
|
+
request = SetWorkerStatusRequest(pid=self.worker_pid, status=status, test_run_id=self.test_run_id)
|
|
573
|
+
self.workers_api.set_worker_status_post(set_worker_status_request=request)
|
|
574
|
+
logging.info(f"Successfully set worker {self.worker_pid} status to {status}")
|
|
575
|
+
except Exception as e:
|
|
576
|
+
logger.error(f"Error setting worker status: {e}", exc_info=False)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@classmethod
|
|
580
|
+
def test_result_to_test_result_cut_api_model(
|
|
581
|
+
cls,
|
|
582
|
+
test_result: TestResult, project_id: str) -> TestResultCutApiModel:
|
|
583
|
+
return TestResultCutApiModel(
|
|
584
|
+
project_id=project_id,
|
|
585
|
+
auto_test_external_id=test_result.get_external_id(),
|
|
586
|
+
status_type=test_result.get_status_type(),
|
|
587
|
+
status_code=test_result.get_outcome(),
|
|
588
|
+
started_on=test_result.get_started_on(),
|
|
589
|
+
)
|