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