Tests: python: path-like object introduced in python 3.6
[lttng-tools.git] / tests / utils / lttngtest / environment.py
CommitLineData
ef945e4d
JG
1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 Jérémie Galarneau <jeremie.galarneau@efficios.com>
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8from types import FrameType
9from typing import Callable, Optional, Tuple, List
10import sys
11import pathlib
12import signal
13import subprocess
14import shlex
15import shutil
16import os
17import queue
18import tempfile
19from . import logger
20import time
21import threading
22import contextlib
23
24
25class TemporaryDirectory:
26 def __init__(self, prefix: str):
27 self._directory_path = tempfile.mkdtemp(prefix=prefix)
28
29 def __del__(self):
30 shutil.rmtree(self._directory_path, ignore_errors=True)
31
32 @property
33 def path(self) -> pathlib.Path:
34 return pathlib.Path(self._directory_path)
35
36
37class _SignalWaitQueue:
38 """
39 Utility class useful to wait for a signal before proceeding.
40
41 Simply register the `signal` method as the handler for the signal you are
42 interested in and call `wait_for_signal` to wait for its reception.
43
44 Registering a signal:
45 signal.signal(signal.SIGWHATEVER, queue.signal)
46
47 Waiting for the signal:
48 queue.wait_for_signal()
49 """
50
51 def __init__(self):
52 self._queue: queue.Queue = queue.Queue()
53
54 def signal(self, signal_number, frame: Optional[FrameType]):
55 self._queue.put_nowait(signal_number)
56
57 def wait_for_signal(self):
58 self._queue.get(block=True)
59
60
61class WaitTraceTestApplication:
62 """
63 Create an application that waits before tracing. This allows a test to
64 launch an application, get its PID, and get it to start tracing when it
65 has completed its setup.
66 """
67
68 def __init__(
69 self,
70 binary_path: pathlib.Path,
71 event_count: int,
72 environment: "Environment",
73 wait_time_between_events_us: int = 0,
74 ):
75 self._environment: Environment = environment
76 if event_count % 5:
77 # The test application currently produces 5 different events per iteration.
78 raise ValueError("event count must be a multiple of 5")
79 self._iteration_count: int = int(event_count / 5)
80 # File that the application will wait to see before tracing its events.
81 self._app_start_tracing_file_path: pathlib.Path = pathlib.Path(
82 tempfile.mktemp(
83 prefix="app_",
84 suffix="_start_tracing",
2d2198ca 85 dir=self._compat_open_path(environment.lttng_home_location),
ef945e4d
JG
86 )
87 )
88 self._has_returned = False
89
90 test_app_env = os.environ.copy()
91 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
92 # Make sure the app is blocked until it is properly registered to
93 # the session daemon.
94 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
95
96 # File that the application will create to indicate it has completed its initialization.
97 app_ready_file_path: str = tempfile.mktemp(
2d2198ca
MJ
98 prefix="app_",
99 suffix="_ready",
100 dir=self._compat_open_path(environment.lttng_home_location),
ef945e4d
JG
101 )
102
103 test_app_args = [str(binary_path)]
104 test_app_args.extend(
105 shlex.split(
106 "--iter {iteration_count} --create-in-main {app_ready_file_path} --wait-before-first-event {app_start_tracing_file_path} --wait {wait_time_between_events_us}".format(
107 iteration_count=self._iteration_count,
108 app_ready_file_path=app_ready_file_path,
109 app_start_tracing_file_path=self._app_start_tracing_file_path,
110 wait_time_between_events_us=wait_time_between_events_us,
111 )
112 )
113 )
114
115 self._process: subprocess.Popen = subprocess.Popen(
116 test_app_args,
117 env=test_app_env,
118 )
119
120 # Wait for the application to create the file indicating it has fully
121 # initialized. Make sure the app hasn't crashed in order to not wait
122 # forever.
123 while True:
124 if os.path.exists(app_ready_file_path):
125 break
126
127 if self._process.poll() is not None:
128 # Application has unexepectedly returned.
129 raise RuntimeError(
130 "Test application has unexepectedly returned during its initialization with return code `{return_code}`".format(
131 return_code=self._process.returncode
132 )
133 )
134
135 time.sleep(0.1)
136
137 def trace(self) -> None:
138 if self._process.poll() is not None:
139 # Application has unexepectedly returned.
140 raise RuntimeError(
141 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
142 return_code=self._process.returncode
143 )
144 )
2d2198ca 145 open(self._compat_open_path(self._app_start_tracing_file_path), mode="x")
ef945e4d
JG
146
147 def wait_for_exit(self) -> None:
148 if self._process.wait() != 0:
149 raise RuntimeError(
150 "Test application has exit with return code `{return_code}`".format(
151 return_code=self._process.returncode
152 )
153 )
154 self._has_returned = True
155
156 @property
157 def vpid(self) -> int:
158 return self._process.pid
159
2d2198ca
MJ
160 @staticmethod
161 def _compat_open_path(path):
162 # type: (pathlib.Path)
163 """
164 The builtin open() in python >= 3.6 expects a path-like object while
165 prior versions expect a string or bytes object. Return the correct type
166 based on the presence of the "__fspath__" attribute specified in PEP-519.
167 """
168 if hasattr(path, "__fspath__"):
169 return path
170 else:
171 return str(path)
172
ef945e4d
JG
173 def __del__(self):
174 if not self._has_returned:
175 # This is potentially racy if the pid has been recycled. However,
176 # we can't use pidfd_open since it is only available in python >= 3.9.
177 self._process.kill()
178 self._process.wait()
179
180
181class ProcessOutputConsumer(threading.Thread, logger._Logger):
182 def __init__(
183 self, process: subprocess.Popen, name: str, log: Callable[[str], None]
184 ):
185 threading.Thread.__init__(self)
186 self._prefix = name
187 logger._Logger.__init__(self, log)
188 self._process = process
189
190 def run(self) -> None:
191 while self._process.poll() is None:
192 assert self._process.stdout
193 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
194 if len(line) != 0:
195 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
196
197
198# Generate a temporary environment in which to execute a test.
199class _Environment(logger._Logger):
200 def __init__(
201 self, with_sessiond: bool, log: Optional[Callable[[str], None]] = None
202 ):
203 super().__init__(log)
204 signal.signal(signal.SIGTERM, self._handle_termination_signal)
205 signal.signal(signal.SIGINT, self._handle_termination_signal)
206
207 # Assumes the project's hierarchy to this file is:
208 # tests/utils/python/this_file
209 self._project_root: pathlib.Path = pathlib.Path(__file__).absolute().parents[3]
210 self._lttng_home: Optional[TemporaryDirectory] = TemporaryDirectory(
211 "lttng_test_env_home"
212 )
213
214 self._sessiond: Optional[subprocess.Popen[bytes]] = (
215 self._launch_lttng_sessiond() if with_sessiond else None
216 )
217
218 @property
219 def lttng_home_location(self) -> pathlib.Path:
220 if self._lttng_home is None:
221 raise RuntimeError("Attempt to access LTTng home after clean-up")
222 return self._lttng_home.path
223
224 @property
225 def lttng_client_path(self) -> pathlib.Path:
226 return self._project_root / "src" / "bin" / "lttng" / "lttng"
227
228 def create_temporary_directory(self, prefix: Optional[str] = None) -> pathlib.Path:
229 # Simply return a path that is contained within LTTNG_HOME; it will
230 # be destroyed when the temporary home goes out of scope.
231 assert self._lttng_home
232 return pathlib.Path(
233 tempfile.mkdtemp(
234 prefix="tmp" if prefix is None else prefix,
235 dir=str(self._lttng_home.path),
236 )
237 )
238
239 # Unpack a list of environment variables from a string
240 # such as "HELLO=is_it ME='/you/are/looking/for'"
241 @staticmethod
242 def _unpack_env_vars(env_vars_string: str) -> List[Tuple[str, str]]:
243 unpacked_vars = []
244 for var in shlex.split(env_vars_string):
245 equal_position = var.find("=")
246 # Must have an equal sign and not end with an equal sign
247 if equal_position == -1 or equal_position == len(var) - 1:
248 raise ValueError(
249 "Invalid sessiond environment variable: `{}`".format(var)
250 )
251
252 var_name = var[0:equal_position]
253 var_value = var[equal_position + 1 :]
254 # Unquote any paths
255 var_value = var_value.replace("'", "")
256 var_value = var_value.replace('"', "")
257 unpacked_vars.append((var_name, var_value))
258
259 return unpacked_vars
260
261 def _launch_lttng_sessiond(self) -> Optional[subprocess.Popen]:
262 is_64bits_host = sys.maxsize > 2**32
263
264 sessiond_path = (
265 self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
266 )
267 consumerd_path_option_name = "--consumerd{bitness}-path".format(
268 bitness="64" if is_64bits_host else "32"
269 )
270 consumerd_path = (
271 self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
272 )
273
274 no_sessiond_var = os.environ.get("TEST_NO_SESSIOND")
275 if no_sessiond_var and no_sessiond_var == "1":
276 # Run test without a session daemon; the user probably
277 # intends to run one under gdb for example.
278 return None
279
280 # Setup the session daemon's environment
281 sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS")
282 sessiond_env = os.environ.copy()
283 if sessiond_env_vars:
284 self._log("Additional lttng-sessiond environment variables:")
285 additional_vars = self._unpack_env_vars(sessiond_env_vars)
286 for var_name, var_value in additional_vars:
287 self._log(" {name}={value}".format(name=var_name, value=var_value))
288 sessiond_env[var_name] = var_value
289
290 sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
291 self._project_root / "src" / "common"
292 )
293
294 assert self._lttng_home is not None
295 sessiond_env["LTTNG_HOME"] = str(self._lttng_home.path)
296
297 wait_queue = _SignalWaitQueue()
298 signal.signal(signal.SIGUSR1, wait_queue.signal)
299
300 self._log(
301 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
302 home_dir=str(self._lttng_home.path)
303 )
304 )
305 process = subprocess.Popen(
306 [
307 str(sessiond_path),
308 consumerd_path_option_name,
309 str(consumerd_path),
310 "--sig-parent",
311 ],
312 stdout=subprocess.PIPE,
313 stderr=subprocess.STDOUT,
314 env=sessiond_env,
315 )
316
317 if self._logging_function:
318 self._sessiond_output_consumer: Optional[
319 ProcessOutputConsumer
320 ] = ProcessOutputConsumer(process, "lttng-sessiond", self._logging_function)
321 self._sessiond_output_consumer.daemon = True
322 self._sessiond_output_consumer.start()
323
324 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
325 wait_queue.wait_for_signal()
326 signal.signal(signal.SIGUSR1, wait_queue.signal)
327
328 return process
329
330 def _handle_termination_signal(
331 self, signal_number: int, frame: Optional[FrameType]
332 ) -> None:
333 self._log(
334 "Killed by {signal_name} signal, cleaning-up".format(
335 signal_name=signal.strsignal(signal_number)
336 )
337 )
338 self._cleanup()
339
340 def launch_wait_trace_test_application(
341 self, event_count: int
342 ) -> WaitTraceTestApplication:
343 """
344 Launch an application that will wait before tracing `event_count` events.
345 """
346 return WaitTraceTestApplication(
347 self._project_root
348 / "tests"
349 / "utils"
350 / "testapp"
351 / "gen-ust-nevents"
352 / "gen-ust-nevents",
353 event_count,
354 self,
355 )
356
357 # Clean-up managed processes
358 def _cleanup(self) -> None:
359 if self._sessiond and self._sessiond.poll() is None:
360 # The session daemon is alive; kill it.
361 self._log(
362 "Killing session daemon (pid = {sessiond_pid})".format(
363 sessiond_pid=self._sessiond.pid
364 )
365 )
366
367 self._sessiond.terminate()
368 self._sessiond.wait()
369 if self._sessiond_output_consumer:
370 self._sessiond_output_consumer.join()
371 self._sessiond_output_consumer = None
372
373 self._log("Session daemon killed")
374 self._sessiond = None
375
376 self._lttng_home = None
377
378 def __del__(self):
379 self._cleanup()
380
381
382@contextlib.contextmanager
383def test_environment(with_sessiond: bool, log: Optional[Callable[[str], None]] = None):
384 env = _Environment(with_sessiond, log)
385 try:
386 yield env
387 finally:
388 env._cleanup()
This page took 0.037223 seconds and 4 git commands to generate.