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
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 79
c661f2f4 80class _WaitTraceTestApplication:
ef945e4d
JG
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
c661f2f4
JG
93 wait_before_exit=False, # type: bool
94 wait_before_exit_file_path=None, # type: Optional[pathlib.Path]
ef945e4d 95 ):
ce8470c9 96 self._environment = environment # type: Environment
ef07b7ae 97 self._iteration_count = event_count
ef945e4d 98 # File that the application will wait to see before tracing its events.
ce8470c9 99 self._app_start_tracing_file_path = pathlib.Path(
ef945e4d
JG
100 tempfile.mktemp(
101 prefix="app_",
102 suffix="_start_tracing",
8a5e3824 103 dir=self._compat_pathlike(environment.lttng_home_location),
ef945e4d
JG
104 )
105 )
c661f2f4
JG
106 # File that the application will create when all events have been emitted.
107 self._app_tracing_done_file_path = pathlib.Path(
108 tempfile.mktemp(
109 prefix="app_",
110 suffix="_done_tracing",
8a5e3824 111 dir=self._compat_pathlike(environment.lttng_home_location),
c661f2f4
JG
112 )
113 )
114
115 if wait_before_exit and wait_before_exit_file_path is None:
116 wait_before_exit_file_path = pathlib.Path(
117 tempfile.mktemp(
118 prefix="app_",
119 suffix="_exit",
8a5e3824 120 dir=self._compat_pathlike(environment.lttng_home_location),
c661f2f4
JG
121 )
122 )
123
ef945e4d
JG
124 self._has_returned = False
125
126 test_app_env = os.environ.copy()
127 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
128 # Make sure the app is blocked until it is properly registered to
129 # the session daemon.
130 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
131
132 # File that the application will create to indicate it has completed its initialization.
8466f071 133 app_ready_file_path = tempfile.mktemp(
2d2198ca
MJ
134 prefix="app_",
135 suffix="_ready",
8a5e3824 136 dir=self._compat_pathlike(environment.lttng_home_location),
ce8470c9 137 ) # type: str
ef945e4d
JG
138
139 test_app_args = [str(binary_path)]
c661f2f4 140 test_app_args.extend(["--iter", str(event_count)])
ef945e4d 141 test_app_args.extend(
c661f2f4
JG
142 ["--sync-application-in-main-touch", str(app_ready_file_path)]
143 )
144 test_app_args.extend(
145 ["--sync-before-first-event", str(self._app_start_tracing_file_path)]
146 )
147 test_app_args.extend(
148 ["--sync-before-exit-touch", str(self._app_tracing_done_file_path)]
ef945e4d 149 )
c661f2f4
JG
150 if wait_time_between_events_us != 0:
151 test_app_args.extend(["--wait", str(wait_time_between_events_us)])
ef945e4d 152
ce8470c9 153 self._process = subprocess.Popen(
ef945e4d
JG
154 test_app_args,
155 env=test_app_env,
c661f2f4
JG
156 stdout=subprocess.PIPE,
157 stderr=subprocess.STDOUT,
ce8470c9 158 ) # type: subprocess.Popen
ef945e4d
JG
159
160 # Wait for the application to create the file indicating it has fully
161 # initialized. Make sure the app hasn't crashed in order to not wait
162 # forever.
c661f2f4
JG
163 self._wait_for_file_to_be_created(pathlib.Path(app_ready_file_path))
164
165 def _wait_for_file_to_be_created(self, sync_file_path):
166 # type: (pathlib.Path) -> None
ef945e4d 167 while True:
8a5e3824 168 if os.path.exists(self._compat_pathlike(sync_file_path)):
ef945e4d
JG
169 break
170
171 if self._process.poll() is not None:
172 # Application has unexepectedly returned.
173 raise RuntimeError(
c661f2f4
JG
174 "Test application has unexepectedly returned while waiting for synchronization file to be created: sync_file=`{sync_file}`, return_code=`{return_code}`".format(
175 sync_file=sync_file_path, return_code=self._process.returncode
ef945e4d
JG
176 )
177 )
178
c661f2f4 179 time.sleep(0.001)
ef945e4d 180
ce8470c9
MJ
181 def trace(self):
182 # type: () -> None
ef945e4d
JG
183 if self._process.poll() is not None:
184 # Application has unexepectedly returned.
185 raise RuntimeError(
186 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
187 return_code=self._process.returncode
188 )
189 )
8a5e3824 190 open(self._compat_pathlike(self._app_start_tracing_file_path), mode="x")
ef945e4d 191
c661f2f4
JG
192 def wait_for_tracing_done(self):
193 # type: () -> None
194 self._wait_for_file_to_be_created(self._app_tracing_done_file_path)
195
ce8470c9
MJ
196 def wait_for_exit(self):
197 # type: () -> None
ef945e4d
JG
198 if self._process.wait() != 0:
199 raise RuntimeError(
200 "Test application has exit with return code `{return_code}`".format(
201 return_code=self._process.returncode
202 )
203 )
204 self._has_returned = True
205
206 @property
ce8470c9
MJ
207 def vpid(self):
208 # type: () -> int
ef945e4d
JG
209 return self._process.pid
210
2d2198ca 211 @staticmethod
8a5e3824 212 def _compat_pathlike(path):
ce8470c9 213 # type: (pathlib.Path) -> pathlib.Path | str
2d2198ca 214 """
8a5e3824
MJ
215 The builtin open() and many methods of the 'os' library in Python >= 3.6
216 expect a path-like object while prior versions expect a string or
217 bytes object. Return the correct type based on the presence of the
218 "__fspath__" attribute specified in PEP-519.
2d2198ca
MJ
219 """
220 if hasattr(path, "__fspath__"):
221 return path
222 else:
223 return str(path)
224
ef945e4d
JG
225 def __del__(self):
226 if not self._has_returned:
227 # This is potentially racy if the pid has been recycled. However,
228 # we can't use pidfd_open since it is only available in python >= 3.9.
229 self._process.kill()
230 self._process.wait()
231
232
c661f2f4
JG
233class WaitTraceTestApplicationGroup:
234 def __init__(
235 self,
236 environment, # type: Environment
237 application_count, # type: int
238 event_count, # type: int
239 wait_time_between_events_us=0, # type: int
240 wait_before_exit=False, # type: bool
241 ):
242 self._wait_before_exit_file_path = (
243 pathlib.Path(
244 tempfile.mktemp(
245 prefix="app_group_",
246 suffix="_exit",
8a5e3824 247 dir=_WaitTraceTestApplication._compat_pathlike(
c661f2f4
JG
248 environment.lttng_home_location
249 ),
250 )
251 )
252 if wait_before_exit
253 else None
254 )
255
256 self._apps = []
257 self._consumers = []
258 for i in range(application_count):
259 new_app = environment.launch_wait_trace_test_application(
260 event_count,
261 wait_time_between_events_us,
262 wait_before_exit,
263 self._wait_before_exit_file_path,
264 )
265
266 # Attach an output consumer to log the application's error output (if any).
267 if environment._logging_function:
268 app_output_consumer = ProcessOutputConsumer(
269 new_app._process,
270 "app-{}".format(str(new_app.vpid)),
271 environment._logging_function,
272 ) # type: Optional[ProcessOutputConsumer]
273 app_output_consumer.daemon = True
274 app_output_consumer.start()
275 self._consumers.append(app_output_consumer)
276
277 self._apps.append(new_app)
278
279 def trace(self):
280 # type: () -> None
281 for app in self._apps:
282 app.trace()
283
284 def exit(
285 self, wait_for_apps=False # type: bool
286 ):
287 if self._wait_before_exit_file_path is None:
288 raise RuntimeError(
289 "Can't call exit on an application group created with `wait_before_exit=False`"
290 )
291
292 # Wait for apps to have produced all of their events so that we can
293 # cause the death of all apps to happen within a short time span.
294 for app in self._apps:
295 app.wait_for_tracing_done()
296
297 open(
8a5e3824 298 _WaitTraceTestApplication._compat_pathlike(
c661f2f4
JG
299 self._wait_before_exit_file_path
300 ),
301 mode="x",
302 )
303 # Performed in two passes to allow tests to stress the unregistration of many applications.
304 # Waiting for each app to exit turn-by-turn would defeat the purpose here.
305 if wait_for_apps:
306 for app in self._apps:
307 app.wait_for_exit()
308
309
310class _TraceTestApplication:
da1e97c9 311 """
e88109fc
JG
312 Create an application that emits events as soon as it is launched. In most
313 scenarios, it is preferable to use a WaitTraceTestApplication.
da1e97c9
MD
314 """
315
873d3601
MJ
316 def __init__(self, binary_path, environment):
317 # type: (pathlib.Path, Environment)
318 self._environment = environment # type: Environment
da1e97c9
MD
319 self._has_returned = False
320
321 test_app_env = os.environ.copy()
322 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
323 # Make sure the app is blocked until it is properly registered to
324 # the session daemon.
325 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
326
327 test_app_args = [str(binary_path)]
328
47ddc6e5 329 self._process = subprocess.Popen(
da1e97c9 330 test_app_args, env=test_app_env
47ddc6e5 331 ) # type: subprocess.Popen
da1e97c9 332
873d3601
MJ
333 def wait_for_exit(self):
334 # type: () -> None
da1e97c9
MD
335 if self._process.wait() != 0:
336 raise RuntimeError(
337 "Test application has exit with return code `{return_code}`".format(
338 return_code=self._process.returncode
339 )
340 )
341 self._has_returned = True
342
343 def __del__(self):
344 if not self._has_returned:
345 # This is potentially racy if the pid has been recycled. However,
346 # we can't use pidfd_open since it is only available in python >= 3.9.
347 self._process.kill()
348 self._process.wait()
349
350
ef945e4d
JG
351class ProcessOutputConsumer(threading.Thread, logger._Logger):
352 def __init__(
ce8470c9
MJ
353 self,
354 process, # type: subprocess.Popen
355 name, # type: str
356 log, # type: Callable[[str], None]
ef945e4d
JG
357 ):
358 threading.Thread.__init__(self)
359 self._prefix = name
360 logger._Logger.__init__(self, log)
361 self._process = process
362
ce8470c9
MJ
363 def run(self):
364 # type: () -> None
ef945e4d
JG
365 while self._process.poll() is None:
366 assert self._process.stdout
367 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
368 if len(line) != 0:
369 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
370
371
372# Generate a temporary environment in which to execute a test.
373class _Environment(logger._Logger):
374 def __init__(
ce8470c9
MJ
375 self,
376 with_sessiond, # type: bool
377 log=None, # type: Optional[Callable[[str], None]]
ef945e4d
JG
378 ):
379 super().__init__(log)
380 signal.signal(signal.SIGTERM, self._handle_termination_signal)
381 signal.signal(signal.SIGINT, self._handle_termination_signal)
382
383 # Assumes the project's hierarchy to this file is:
384 # tests/utils/python/this_file
ce8470c9
MJ
385 self._project_root = (
386 pathlib.Path(__file__).absolute().parents[3]
387 ) # type: pathlib.Path
388 self._lttng_home = TemporaryDirectory(
ef945e4d 389 "lttng_test_env_home"
ce8470c9 390 ) # type: Optional[TemporaryDirectory]
ef945e4d 391
ce8470c9 392 self._sessiond = (
ef945e4d 393 self._launch_lttng_sessiond() if with_sessiond else None
ce8470c9 394 ) # type: Optional[subprocess.Popen[bytes]]
ef945e4d
JG
395
396 @property
ce8470c9
MJ
397 def lttng_home_location(self):
398 # type: () -> pathlib.Path
ef945e4d
JG
399 if self._lttng_home is None:
400 raise RuntimeError("Attempt to access LTTng home after clean-up")
401 return self._lttng_home.path
402
403 @property
ce8470c9
MJ
404 def lttng_client_path(self):
405 # type: () -> pathlib.Path
ef945e4d
JG
406 return self._project_root / "src" / "bin" / "lttng" / "lttng"
407
ce8470c9
MJ
408 def create_temporary_directory(self, prefix=None):
409 # type: (Optional[str]) -> pathlib.Path
ef945e4d
JG
410 # Simply return a path that is contained within LTTNG_HOME; it will
411 # be destroyed when the temporary home goes out of scope.
412 assert self._lttng_home
413 return pathlib.Path(
414 tempfile.mkdtemp(
415 prefix="tmp" if prefix is None else prefix,
416 dir=str(self._lttng_home.path),
417 )
418 )
419
420 # Unpack a list of environment variables from a string
421 # such as "HELLO=is_it ME='/you/are/looking/for'"
422 @staticmethod
ce8470c9
MJ
423 def _unpack_env_vars(env_vars_string):
424 # type: (str) -> List[Tuple[str, str]]
ef945e4d
JG
425 unpacked_vars = []
426 for var in shlex.split(env_vars_string):
427 equal_position = var.find("=")
428 # Must have an equal sign and not end with an equal sign
429 if equal_position == -1 or equal_position == len(var) - 1:
430 raise ValueError(
431 "Invalid sessiond environment variable: `{}`".format(var)
432 )
433
434 var_name = var[0:equal_position]
435 var_value = var[equal_position + 1 :]
436 # Unquote any paths
437 var_value = var_value.replace("'", "")
438 var_value = var_value.replace('"', "")
439 unpacked_vars.append((var_name, var_value))
440
441 return unpacked_vars
442
ce8470c9
MJ
443 def _launch_lttng_sessiond(self):
444 # type: () -> Optional[subprocess.Popen]
ef945e4d
JG
445 is_64bits_host = sys.maxsize > 2**32
446
447 sessiond_path = (
448 self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
449 )
450 consumerd_path_option_name = "--consumerd{bitness}-path".format(
451 bitness="64" if is_64bits_host else "32"
452 )
453 consumerd_path = (
454 self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
455 )
456
457 no_sessiond_var = os.environ.get("TEST_NO_SESSIOND")
458 if no_sessiond_var and no_sessiond_var == "1":
459 # Run test without a session daemon; the user probably
460 # intends to run one under gdb for example.
461 return None
462
463 # Setup the session daemon's environment
464 sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS")
465 sessiond_env = os.environ.copy()
466 if sessiond_env_vars:
467 self._log("Additional lttng-sessiond environment variables:")
468 additional_vars = self._unpack_env_vars(sessiond_env_vars)
469 for var_name, var_value in additional_vars:
470 self._log(" {name}={value}".format(name=var_name, value=var_value))
471 sessiond_env[var_name] = var_value
472
473 sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
474 self._project_root / "src" / "common"
475 )
476
477 assert self._lttng_home is not None
478 sessiond_env["LTTNG_HOME"] = str(self._lttng_home.path)
479
480 wait_queue = _SignalWaitQueue()
0ac0f70e
JG
481 with wait_queue.intercept_signal(signal.SIGUSR1):
482 self._log(
483 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
484 home_dir=str(self._lttng_home.path)
485 )
486 )
487 process = subprocess.Popen(
488 [
489 str(sessiond_path),
490 consumerd_path_option_name,
491 str(consumerd_path),
492 "--sig-parent",
493 ],
494 stdout=subprocess.PIPE,
495 stderr=subprocess.STDOUT,
496 env=sessiond_env,
ef945e4d 497 )
ef945e4d 498
0ac0f70e
JG
499 if self._logging_function:
500 self._sessiond_output_consumer = ProcessOutputConsumer(
501 process, "lttng-sessiond", self._logging_function
502 ) # type: Optional[ProcessOutputConsumer]
503 self._sessiond_output_consumer.daemon = True
504 self._sessiond_output_consumer.start()
ef945e4d 505
0ac0f70e
JG
506 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
507 wait_queue.wait_for_signal()
ef945e4d
JG
508
509 return process
510
ce8470c9
MJ
511 def _handle_termination_signal(self, signal_number, frame):
512 # type: (int, Optional[FrameType]) -> None
ef945e4d
JG
513 self._log(
514 "Killed by {signal_name} signal, cleaning-up".format(
515 signal_name=signal.strsignal(signal_number)
516 )
517 )
518 self._cleanup()
519
c661f2f4
JG
520 def launch_wait_trace_test_application(
521 self,
522 event_count, # type: int
523 wait_time_between_events_us=0,
524 wait_before_exit=False,
525 wait_before_exit_file_path=None,
526 ):
527 # type: (int, int, bool, Optional[pathlib.Path]) -> _WaitTraceTestApplication
ef945e4d
JG
528 """
529 Launch an application that will wait before tracing `event_count` events.
530 """
c661f2f4 531 return _WaitTraceTestApplication(
ef945e4d
JG
532 self._project_root
533 / "tests"
534 / "utils"
535 / "testapp"
ef07b7ae
JG
536 / "gen-ust-events"
537 / "gen-ust-events",
ef945e4d
JG
538 event_count,
539 self,
c661f2f4
JG
540 wait_time_between_events_us,
541 wait_before_exit,
542 wait_before_exit_file_path,
ef945e4d
JG
543 )
544
873d3601
MJ
545 def launch_trace_test_constructor_application(self):
546 # type () -> TraceTestApplication
da1e97c9
MD
547 """
548 Launch an application that will trace from within constructors.
549 """
c661f2f4 550 return _TraceTestApplication(
da1e97c9
MD
551 self._project_root
552 / "tests"
553 / "utils"
554 / "testapp"
555 / "gen-ust-events-constructor"
556 / "gen-ust-events-constructor",
557 self,
558 )
559
ef945e4d 560 # Clean-up managed processes
ce8470c9
MJ
561 def _cleanup(self):
562 # type: () -> None
ef945e4d
JG
563 if self._sessiond and self._sessiond.poll() is None:
564 # The session daemon is alive; kill it.
565 self._log(
566 "Killing session daemon (pid = {sessiond_pid})".format(
567 sessiond_pid=self._sessiond.pid
568 )
569 )
570
571 self._sessiond.terminate()
572 self._sessiond.wait()
573 if self._sessiond_output_consumer:
574 self._sessiond_output_consumer.join()
575 self._sessiond_output_consumer = None
576
577 self._log("Session daemon killed")
578 self._sessiond = None
579
580 self._lttng_home = None
581
582 def __del__(self):
583 self._cleanup()
584
585
586@contextlib.contextmanager
ce8470c9
MJ
587def test_environment(with_sessiond, log=None):
588 # type: (bool, Optional[Callable[[str], None]]) -> Iterator[_Environment]
ef945e4d
JG
589 env = _Environment(with_sessiond, log)
590 try:
591 yield env
592 finally:
593 env._cleanup()
This page took 0.053855 seconds and 4 git commands to generate.