Tests: Fix: Use '.logfile' instead of '.log' for test app output
[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
03775d44
KS
12import pwd
13import random
ef945e4d 14import signal
91118dcc 15import socket
ef945e4d
JG
16import subprocess
17import shlex
18import shutil
03775d44
KS
19import stat
20import string
ef945e4d
JG
21import os
22import queue
23import tempfile
24from . import logger
25import time
26import threading
27import contextlib
28
91118dcc
KS
29import bt2
30
ef945e4d
JG
31
32class TemporaryDirectory:
ce8470c9
MJ
33 def __init__(self, prefix):
34 # type: (str) -> None
ef945e4d
JG
35 self._directory_path = tempfile.mkdtemp(prefix=prefix)
36
37 def __del__(self):
03775d44
KS
38 if os.getenv("LTTNG_TEST_PRESERVE_TEST_ENV", "0") != "1":
39 shutil.rmtree(self._directory_path, ignore_errors=True)
ef945e4d
JG
40
41 @property
ce8470c9
MJ
42 def path(self):
43 # type: () -> pathlib.Path
ef945e4d
JG
44 return pathlib.Path(self._directory_path)
45
46
47class _SignalWaitQueue:
48 """
49 Utility class useful to wait for a signal before proceeding.
50
51 Simply register the `signal` method as the handler for the signal you are
52 interested in and call `wait_for_signal` to wait for its reception.
53
54 Registering a signal:
55 signal.signal(signal.SIGWHATEVER, queue.signal)
56
57 Waiting for the signal:
58 queue.wait_for_signal()
59 """
60
61 def __init__(self):
ce8470c9 62 self._queue = queue.Queue() # type: queue.Queue
ef945e4d 63
ce8470c9
MJ
64 def signal(
65 self,
66 signal_number,
67 frame, # type: Optional[FrameType]
68 ):
ef945e4d
JG
69 self._queue.put_nowait(signal_number)
70
71 def wait_for_signal(self):
72 self._queue.get(block=True)
73
0ac0f70e
JG
74 @contextlib.contextmanager
75 def intercept_signal(self, signal_number):
76 # type: (int) -> Generator[None, None, None]
77 original_handler = signal.getsignal(signal_number)
78 signal.signal(signal_number, self.signal)
79 try:
80 yield
81 except:
82 # Restore the original signal handler and forward the exception.
83 raise
84 finally:
85 signal.signal(signal_number, original_handler)
86
ef945e4d 87
91118dcc
KS
88class _LiveViewer:
89 """
90 Create a babeltrace2 live viewer.
91 """
92
93 def __init__(
94 self,
95 environment, # type: Environment
96 session, # type: str
97 hostname=None, # type: Optional[str]
98 ):
99 self._environment = environment
100 self._session = session
101 self._hostname = hostname
102 if self._hostname is None:
103 self._hostname = socket.gethostname()
104 self._events = []
105
106 ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"]
107 self._live_iterator = bt2.TraceCollectionMessageIterator(
108 bt2.ComponentSpec(
109 ctf_live_cc,
110 {
111 "inputs": [
112 "net://localhost:{}/host/{}/{}".format(
113 environment.lttng_relayd_live_port,
114 self._hostname,
115 session,
116 )
117 ],
118 "session-not-found-action": "end",
119 },
120 )
121 )
122
123 try:
124 # Cause the connection to be initiated since tests
125 # tend to wait for a viewer to be connected before proceeding.
126 msg = next(self._live_iterator)
127 self._events.append(msg)
128 except bt2.TryAgain:
129 pass
130
131 @property
132 def output(self):
133 return self._events
134
135 @property
136 def messages(self):
137 return [x for x in self._events if type(x) is bt2._EventMessageConst]
138
139 def _drain(self, retry=False):
140 while True:
141 try:
142 for msg in self._live_iterator:
c6183a33
KS
143 if type(msg) is bt2._MessageIteratorInactivityMessageConst:
144 break
91118dcc
KS
145 self._events.append(msg)
146 break
147 except bt2.TryAgain as e:
148 if retry:
149 time.sleep(0.01)
150 continue
151 else:
152 break
153
b0c95099 154 def is_connected(self):
91118dcc
KS
155 ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"]
156 self._environment._log(
157 "Checking for connected clients at 'net://localhost:{}'".format(
158 self._environment.lttng_relayd_live_port
159 )
160 )
161 query_executor = bt2.QueryExecutor(
162 ctf_live_cc,
163 "sessions",
164 params={
165 "url": "net://localhost:{}".format(
166 self._environment.lttng_relayd_live_port
167 )
168 },
169 )
b0c95099
KS
170
171 for live_session in query_executor.query():
172 if (
173 live_session["session-name"] == self._session
174 and live_session["client-count"] >= 1
175 ):
176 self._environment._log(
177 "Session '{}' has {} connected clients".format(
178 live_session["session-name"], live_session["client-count"]
179 )
180 )
181 return True
182 return False
183
7b874e71
KS
184 def _wait_until(self, desired_state: bool, timeout=0):
185 connected_state = not desired_state
91118dcc 186 started = time.time()
7b874e71 187 while connected_state != desired_state:
91118dcc
KS
188 try:
189 if timeout != 0 and (time.time() - started) > timeout:
190 raise RuntimeError(
191 "Timed out waiting for connected clients on session '{}' after {}s".format(
192 self._session, time.time() - started
193 )
194 )
b0c95099 195
7b874e71 196 connected_state = self.is_connected()
91118dcc
KS
197 except bt2._Error:
198 time.sleep(0.01)
199 continue
7b874e71
KS
200 return connected_state
201
202 def wait_until_disconnected(self, timeout=0):
203 return self._wait_until(False, timeout)
204
205 def wait_until_connected(self, timeout=0):
206 return self._wait_until(True, timeout)
91118dcc
KS
207
208 def wait(self):
209 if self._live_iterator:
210 self._drain(retry=True)
211 del self._live_iterator
212 self._live_iterator = None
213
214 def __del__(self):
215 pass
216
217
c661f2f4 218class _WaitTraceTestApplication:
ef945e4d
JG
219 """
220 Create an application that waits before tracing. This allows a test to
221 launch an application, get its PID, and get it to start tracing when it
222 has completed its setup.
223 """
224
225 def __init__(
226 self,
ce8470c9
MJ
227 binary_path, # type: pathlib.Path
228 event_count, # type: int
229 environment, # type: Environment
230 wait_time_between_events_us=0, # type: int
c661f2f4
JG
231 wait_before_exit=False, # type: bool
232 wait_before_exit_file_path=None, # type: Optional[pathlib.Path]
03775d44 233 run_as=None, # type: Optional[str]
ef945e4d 234 ):
cebde614 235 self._process = None
ce8470c9 236 self._environment = environment # type: Environment
ef07b7ae 237 self._iteration_count = event_count
ef945e4d 238 # File that the application will wait to see before tracing its events.
11ababae
KS
239 dir = (
240 self._compat_pathlike(environment.lttng_home_location)
241 if environment.lttng_home_location
242 else None
243 )
03775d44
KS
244 if run_as is not None:
245 dir = os.path.join(dir, run_as)
ce8470c9 246 self._app_start_tracing_file_path = pathlib.Path(
ef945e4d
JG
247 tempfile.mktemp(
248 prefix="app_",
249 suffix="_start_tracing",
03775d44 250 dir=dir,
ef945e4d
JG
251 )
252 )
03775d44 253
c661f2f4
JG
254 # File that the application will create when all events have been emitted.
255 self._app_tracing_done_file_path = pathlib.Path(
256 tempfile.mktemp(
257 prefix="app_",
258 suffix="_done_tracing",
03775d44 259 dir=dir,
c661f2f4
JG
260 )
261 )
262
263 if wait_before_exit and wait_before_exit_file_path is None:
264 wait_before_exit_file_path = pathlib.Path(
265 tempfile.mktemp(
266 prefix="app_",
267 suffix="_exit",
c0aaf21b 268 dir=dir,
c661f2f4
JG
269 )
270 )
c0aaf21b 271 self._wait_before_exit_file_path = wait_before_exit_file_path
ef945e4d 272 self._has_returned = False
c0aaf21b 273 self._tracing_started = False
ef945e4d
JG
274
275 test_app_env = os.environ.copy()
11ababae
KS
276 if environment.lttng_home_location is not None:
277 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
ef945e4d
JG
278 # Make sure the app is blocked until it is properly registered to
279 # the session daemon.
280 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
281
282 # File that the application will create to indicate it has completed its initialization.
8466f071 283 app_ready_file_path = tempfile.mktemp(
2d2198ca
MJ
284 prefix="app_",
285 suffix="_ready",
03775d44 286 dir=dir,
ce8470c9 287 ) # type: str
ef945e4d
JG
288
289 test_app_args = [str(binary_path)]
c661f2f4 290 test_app_args.extend(["--iter", str(event_count)])
ef945e4d 291 test_app_args.extend(
c661f2f4
JG
292 ["--sync-application-in-main-touch", str(app_ready_file_path)]
293 )
294 test_app_args.extend(
295 ["--sync-before-first-event", str(self._app_start_tracing_file_path)]
296 )
297 test_app_args.extend(
298 ["--sync-before-exit-touch", str(self._app_tracing_done_file_path)]
ef945e4d 299 )
c0aaf21b
KS
300 if wait_before_exit:
301 test_app_args.extend(
302 ["--sync-before-exit", str(self._wait_before_exit_file_path)]
303 )
c661f2f4
JG
304 if wait_time_between_events_us != 0:
305 test_app_args.extend(["--wait", str(wait_time_between_events_us)])
ef945e4d 306
03775d44
KS
307 if run_as is not None:
308 # When running as root and reducing the permissions to run as another
309 # user, the test binary needs to be readable and executable by the
310 # world; however, the file may be in a deep path or on systems where
311 # we don't want to modify the filesystem state (eg. for a person who
312 # has downloaded and ran the tests manually).
313 # Therefore, the binary_path is copied to a temporary file in the
314 # `run_as` user's home directory
315 new_binary_path = os.path.join(
316 str(environment.lttng_home_location),
317 run_as,
318 os.path.basename(str(binary_path)),
319 )
320
321 if not os.path.exists(new_binary_path):
322 shutil.copy(str(binary_path), new_binary_path)
323
324 test_app_args[0] = new_binary_path
325
326 lib_dir = environment.lttng_home_location / run_as / "lib"
327 if not os.path.isdir(str(lib_dir)):
328 os.mkdir(str(lib_dir))
329 # When running dropping privileges, the libraries built in the
330 # root-owned directories may not be reachable and readable by
331 # the loader running as an unprivileged user. These should also be
332 # copied.
333 _ldd = subprocess.Popen(
334 ["ldd", new_binary_path],
335 stdout=subprocess.PIPE,
336 stderr=subprocess.PIPE,
337 )
338 if _ldd.wait() != 0:
339 raise RuntimeError(
340 "Error while using `ldd` to determine test application dependencies: `{}`".format(
341 stderr.read().decode("utf-8")
342 )
343 )
344 libs = [
345 x.decode("utf-8").split(sep="=>") for x in _ldd.stdout.readlines()
346 ]
347 libs = [
348 x[1].split(sep=" ")[1]
349 for x in libs
350 if len(x) >= 2 and x[1].find("lttng") != -1
351 ]
352 for lib in libs:
353 shutil.copy(lib, lib_dir)
354
355 test_app_env["LD_LIBRARY_PATH"] = "{}:{}".format(
356 test_app_env["LD_LIBRARY_PATH"],
357 str(lib_dir),
358 )
359
360 # As of python 3.9, subprocess.Popen supports a user parameter which
361 # runs `setreuid()` before executing the proces and will be preferable
362 # when support for older python versions is no longer required.
363 test_app_args = [
364 "runuser",
365 "-u",
366 run_as,
367 "--",
368 ] + test_app_args
369
370 self._environment._log(
371 "Launching test application: '{}'".format(
372 self._compat_shlex_join(test_app_args)
373 )
374 )
ce8470c9 375 self._process = subprocess.Popen(
ef945e4d
JG
376 test_app_args,
377 env=test_app_env,
c661f2f4 378 stdout=subprocess.PIPE,
91118dcc 379 stderr=subprocess.PIPE,
ce8470c9 380 ) # type: subprocess.Popen
ef945e4d
JG
381
382 # Wait for the application to create the file indicating it has fully
383 # initialized. Make sure the app hasn't crashed in order to not wait
384 # forever.
c661f2f4
JG
385 self._wait_for_file_to_be_created(pathlib.Path(app_ready_file_path))
386
387 def _wait_for_file_to_be_created(self, sync_file_path):
388 # type: (pathlib.Path) -> None
ef945e4d 389 while True:
8a5e3824 390 if os.path.exists(self._compat_pathlike(sync_file_path)):
ef945e4d
JG
391 break
392
393 if self._process.poll() is not None:
394 # Application has unexepectedly returned.
395 raise RuntimeError(
03775d44
KS
396 "Test application has unexepectedly returned while waiting for synchronization file to be created: sync_file=`{sync_file}`, return_code=`{return_code}`, output=`{output}`".format(
397 sync_file=sync_file_path,
398 return_code=self._process.returncode,
399 output=self._process.stderr.read().decode("utf-8"),
ef945e4d
JG
400 )
401 )
402
c661f2f4 403 time.sleep(0.001)
ef945e4d 404
c0aaf21b
KS
405 def touch_exit_file(self):
406 open(self._compat_pathlike(self._wait_before_exit_file_path), mode="x")
407
ce8470c9
MJ
408 def trace(self):
409 # type: () -> None
ef945e4d
JG
410 if self._process.poll() is not None:
411 # Application has unexepectedly returned.
412 raise RuntimeError(
413 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
414 return_code=self._process.returncode
415 )
416 )
8a5e3824 417 open(self._compat_pathlike(self._app_start_tracing_file_path), mode="x")
c0aaf21b
KS
418 self._environment._log("[{}] Tracing started".format(self.vpid))
419 self._tracing_started = True
ef945e4d 420
c661f2f4
JG
421 def wait_for_tracing_done(self):
422 # type: () -> None
c0aaf21b
KS
423 if not self._tracing_started:
424 raise RuntimeError("Tracing hasn't been started")
c661f2f4 425 self._wait_for_file_to_be_created(self._app_tracing_done_file_path)
c0aaf21b 426 self._environment._log("[{}] Tracing done".format(self.vpid))
c661f2f4 427
ce8470c9
MJ
428 def wait_for_exit(self):
429 # type: () -> None
ef945e4d
JG
430 if self._process.wait() != 0:
431 raise RuntimeError(
03775d44
KS
432 "Test application [{pid}] has exit with return code `{return_code}`, output=`{output}`".format(
433 pid=self.vpid,
434 return_code=self._process.returncode,
435 output=self._process.stderr.read().decode("utf-8"),
ef945e4d
JG
436 )
437 )
438 self._has_returned = True
439
440 @property
ce8470c9
MJ
441 def vpid(self):
442 # type: () -> int
ef945e4d
JG
443 return self._process.pid
444
2d2198ca 445 @staticmethod
8a5e3824 446 def _compat_pathlike(path):
ce8470c9 447 # type: (pathlib.Path) -> pathlib.Path | str
2d2198ca 448 """
8a5e3824
MJ
449 The builtin open() and many methods of the 'os' library in Python >= 3.6
450 expect a path-like object while prior versions expect a string or
451 bytes object. Return the correct type based on the presence of the
452 "__fspath__" attribute specified in PEP-519.
2d2198ca
MJ
453 """
454 if hasattr(path, "__fspath__"):
455 return path
456 else:
457 return str(path)
458
03775d44
KS
459 @staticmethod
460 def _compat_shlex_join(args):
461 # type: list[str] -> str
462 # shlex.join was added in python 3.8
463 return " ".join([shlex.quote(x) for x in args])
464
ef945e4d 465 def __del__(self):
cebde614 466 if self._process is not None and not self._has_returned:
ef945e4d
JG
467 # This is potentially racy if the pid has been recycled. However,
468 # we can't use pidfd_open since it is only available in python >= 3.9.
469 self._process.kill()
470 self._process.wait()
471
472
c661f2f4
JG
473class WaitTraceTestApplicationGroup:
474 def __init__(
475 self,
476 environment, # type: Environment
477 application_count, # type: int
478 event_count, # type: int
479 wait_time_between_events_us=0, # type: int
480 wait_before_exit=False, # type: bool
481 ):
482 self._wait_before_exit_file_path = (
483 pathlib.Path(
484 tempfile.mktemp(
485 prefix="app_group_",
486 suffix="_exit",
8a5e3824 487 dir=_WaitTraceTestApplication._compat_pathlike(
c661f2f4
JG
488 environment.lttng_home_location
489 ),
490 )
491 )
492 if wait_before_exit
493 else None
494 )
495
496 self._apps = []
497 self._consumers = []
498 for i in range(application_count):
499 new_app = environment.launch_wait_trace_test_application(
500 event_count,
501 wait_time_between_events_us,
502 wait_before_exit,
503 self._wait_before_exit_file_path,
504 )
505
506 # Attach an output consumer to log the application's error output (if any).
507 if environment._logging_function:
508 app_output_consumer = ProcessOutputConsumer(
509 new_app._process,
510 "app-{}".format(str(new_app.vpid)),
511 environment._logging_function,
512 ) # type: Optional[ProcessOutputConsumer]
513 app_output_consumer.daemon = True
514 app_output_consumer.start()
515 self._consumers.append(app_output_consumer)
516
517 self._apps.append(new_app)
518
519 def trace(self):
520 # type: () -> None
521 for app in self._apps:
522 app.trace()
523
524 def exit(
525 self, wait_for_apps=False # type: bool
526 ):
527 if self._wait_before_exit_file_path is None:
528 raise RuntimeError(
529 "Can't call exit on an application group created with `wait_before_exit=False`"
530 )
531
532 # Wait for apps to have produced all of their events so that we can
533 # cause the death of all apps to happen within a short time span.
534 for app in self._apps:
535 app.wait_for_tracing_done()
536
c0aaf21b
KS
537 self._apps[0].touch_exit_file()
538
c661f2f4
JG
539 # Performed in two passes to allow tests to stress the unregistration of many applications.
540 # Waiting for each app to exit turn-by-turn would defeat the purpose here.
541 if wait_for_apps:
542 for app in self._apps:
543 app.wait_for_exit()
544
545
546class _TraceTestApplication:
da1e97c9 547 """
e88109fc
JG
548 Create an application that emits events as soon as it is launched. In most
549 scenarios, it is preferable to use a WaitTraceTestApplication.
da1e97c9
MD
550 """
551
873d3601
MJ
552 def __init__(self, binary_path, environment):
553 # type: (pathlib.Path, Environment)
cebde614 554 self._process = None
873d3601 555 self._environment = environment # type: Environment
da1e97c9
MD
556 self._has_returned = False
557
558 test_app_env = os.environ.copy()
559 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
560 # Make sure the app is blocked until it is properly registered to
561 # the session daemon.
562 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
563
564 test_app_args = [str(binary_path)]
565
47ddc6e5 566 self._process = subprocess.Popen(
da1e97c9 567 test_app_args, env=test_app_env
47ddc6e5 568 ) # type: subprocess.Popen
da1e97c9 569
873d3601
MJ
570 def wait_for_exit(self):
571 # type: () -> None
da1e97c9
MD
572 if self._process.wait() != 0:
573 raise RuntimeError(
574 "Test application has exit with return code `{return_code}`".format(
575 return_code=self._process.returncode
576 )
577 )
578 self._has_returned = True
579
580 def __del__(self):
cebde614 581 if self._process is not None and not self._has_returned:
da1e97c9
MD
582 # This is potentially racy if the pid has been recycled. However,
583 # we can't use pidfd_open since it is only available in python >= 3.9.
584 self._process.kill()
585 self._process.wait()
586
587
ef945e4d
JG
588class ProcessOutputConsumer(threading.Thread, logger._Logger):
589 def __init__(
ce8470c9
MJ
590 self,
591 process, # type: subprocess.Popen
592 name, # type: str
593 log, # type: Callable[[str], None]
ef945e4d
JG
594 ):
595 threading.Thread.__init__(self)
596 self._prefix = name
597 logger._Logger.__init__(self, log)
598 self._process = process
599
ce8470c9
MJ
600 def run(self):
601 # type: () -> None
ef945e4d
JG
602 while self._process.poll() is None:
603 assert self._process.stdout
604 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
605 if len(line) != 0:
606 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
607
608
91118dcc
KS
609class SavingProcessOutputConsumer(ProcessOutputConsumer):
610 def __init__(self, process, name, log):
611 self._lines = []
612 super().__init__(process=process, name=name, log=log)
613
614 def run(self):
615 # type: () -> None
616 while self._process.poll() is None:
617 assert self._process.stdout
618 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
619 if len(line) != 0:
620 self._lines.append(line)
621 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
622
623 @property
624 def output(self):
625 return self._lines
626
627
ef945e4d
JG
628# Generate a temporary environment in which to execute a test.
629class _Environment(logger._Logger):
630 def __init__(
ce8470c9
MJ
631 self,
632 with_sessiond, # type: bool
633 log=None, # type: Optional[Callable[[str], None]]
45ce5eed 634 with_relayd=False, # type: bool
11ababae
KS
635 extra_env_vars=dict(), # type: dict
636 skip_temporary_lttng_home=False, # type: bool
ef945e4d
JG
637 ):
638 super().__init__(log)
639 signal.signal(signal.SIGTERM, self._handle_termination_signal)
640 signal.signal(signal.SIGINT, self._handle_termination_signal)
641
55aec41f
KS
642 if os.getenv("LTTNG_TEST_VERBOSE_BABELTRACE", "0") == "1":
643 # @TODO: Is there a way to feed the logging output to
644 # the logger._Logger instead of directly to stderr?
645 bt2.set_global_logging_level(bt2.LoggingLevel.TRACE)
646
ef945e4d
JG
647 # Assumes the project's hierarchy to this file is:
648 # tests/utils/python/this_file
ce8470c9
MJ
649 self._project_root = (
650 pathlib.Path(__file__).absolute().parents[3]
651 ) # type: pathlib.Path
03775d44 652
11ababae
KS
653 self._extra_env_vars = extra_env_vars
654
655 # There are times when we need to exercise default configurations
656 # that don't set LTTNG_HOME. When doing this, it makes it impossible
657 # to safely run parallel tests.
658 self._lttng_home = None
659 if not skip_temporary_lttng_home:
660 self._lttng_home = TemporaryDirectory(
661 "lttng_test_env_home"
662 ) # type: Optional[TemporaryDirectory]
663 os.chmod(
664 str(self._lttng_home.path),
665 stat.S_IRUSR
666 | stat.S_IWUSR
667 | stat.S_IXUSR
668 | stat.S_IROTH
669 | stat.S_IXOTH,
670 )
ef945e4d 671
45ce5eed
KS
672 self._relayd = (
673 self._launch_lttng_relayd() if with_relayd else None
674 ) # type: Optional[subprocess.Popen[bytes]]
675 self._relayd_output_consumer = None
676
ce8470c9 677 self._sessiond = (
ef945e4d 678 self._launch_lttng_sessiond() if with_sessiond else None
ce8470c9 679 ) # type: Optional[subprocess.Popen[bytes]]
ef945e4d 680
03775d44
KS
681 self._dummy_users = {} # type: Dictionary[int, string]
682 self._preserve_test_env = os.getenv("LTTNG_TEST_PRESERVE_TEST_ENV", "0") != "1"
683
ef945e4d 684 @property
ce8470c9
MJ
685 def lttng_home_location(self):
686 # type: () -> pathlib.Path
11ababae
KS
687 if self._lttng_home is not None:
688 return self._lttng_home.path
689 return None
ef945e4d
JG
690
691 @property
ce8470c9
MJ
692 def lttng_client_path(self):
693 # type: () -> pathlib.Path
ef945e4d
JG
694 return self._project_root / "src" / "bin" / "lttng" / "lttng"
695
45ce5eed
KS
696 @property
697 def lttng_relayd_control_port(self):
698 # type: () -> int
699 return 5400
700
701 @property
702 def lttng_relayd_data_port(self):
703 # type: () -> int
704 return 5401
705
706 @property
707 def lttng_relayd_live_port(self):
708 # type: () -> int
709 return 5402
710
03775d44
KS
711 @property
712 def preserve_test_env(self):
713 # type: () -> bool
714 return self._preserve_test_env
715
716 @staticmethod
717 def allows_destructive():
718 # type: () -> bool
719 return os.getenv("LTTNG_ENABLE_DESTRUCTIVE_TESTS", "") == "will-break-my-system"
720
721 def create_dummy_user(self):
722 # type: () -> (int, str)
723 # Create a dummy user. The uid and username will be eturned in a tuple.
724 # If the name already exists, an exception will be thrown.
725 # The users will be removed when the environment is cleaned up.
726 name = "".join([random.choice(string.ascii_lowercase) for x in range(10)])
727
728 try:
729 entry = pwd.getpwnam(name)
730 raise Exception("User '{}' already exists".format(name))
731 except KeyError:
732 pass
733
734 # Create user
735 proc = subprocess.Popen(
736 [
737 "useradd",
738 "--base-dir",
739 str(self._lttng_home.path),
740 "--create-home",
741 "--no-user-group",
742 "--shell",
743 "/bin/sh",
744 name,
745 ]
746 )
747 proc.wait()
748 if proc.returncode != 0:
749 raise Exception(
750 "Failed to create user '{}', useradd returned {}".format(
751 name, proc.returncode
752 )
753 )
754
755 entry = pwd.getpwnam(name)
756 self._dummy_users[entry[2]] = name
757 return (entry[2], name)
758
ce8470c9
MJ
759 def create_temporary_directory(self, prefix=None):
760 # type: (Optional[str]) -> pathlib.Path
ef945e4d
JG
761 # Simply return a path that is contained within LTTNG_HOME; it will
762 # be destroyed when the temporary home goes out of scope.
ef945e4d
JG
763 return pathlib.Path(
764 tempfile.mkdtemp(
765 prefix="tmp" if prefix is None else prefix,
11ababae 766 dir=str(self.lttng_home_location) if self.lttng_home_location else None,
ef945e4d
JG
767 )
768 )
769
11ababae
KS
770 @staticmethod
771 def run_kernel_tests():
772 # type: () -> bool
773 return (
774 os.getenv("LTTNG_TOOLS_DISABLE_KERNEL_TESTS", "0") != "1"
775 and os.getuid() == 0
776 )
777
ef945e4d
JG
778 # Unpack a list of environment variables from a string
779 # such as "HELLO=is_it ME='/you/are/looking/for'"
780 @staticmethod
ce8470c9
MJ
781 def _unpack_env_vars(env_vars_string):
782 # type: (str) -> List[Tuple[str, str]]
ef945e4d
JG
783 unpacked_vars = []
784 for var in shlex.split(env_vars_string):
785 equal_position = var.find("=")
786 # Must have an equal sign and not end with an equal sign
787 if equal_position == -1 or equal_position == len(var) - 1:
788 raise ValueError(
789 "Invalid sessiond environment variable: `{}`".format(var)
790 )
791
792 var_name = var[0:equal_position]
793 var_value = var[equal_position + 1 :]
794 # Unquote any paths
795 var_value = var_value.replace("'", "")
796 var_value = var_value.replace('"', "")
797 unpacked_vars.append((var_name, var_value))
798
799 return unpacked_vars
800
45ce5eed
KS
801 def _launch_lttng_relayd(self):
802 # type: () -> Optional[subprocess.Popen]
803 relayd_path = (
804 self._project_root / "src" / "bin" / "lttng-relayd" / "lttng-relayd"
805 )
806 if os.environ.get("LTTNG_TEST_NO_RELAYD", "0") == "1":
807 # Run without a relay daemon; the user may be running one
808 # under gdb, for example.
809 return None
810
811 relayd_env_vars = os.environ.get("LTTNG_RELAYD_ENV_VARS")
812 relayd_env = os.environ.copy()
11ababae 813 relayd_env.update(self._extra_env_vars)
45ce5eed
KS
814 if relayd_env_vars:
815 self._log("Additional lttng-relayd environment variables:")
816 for name, value in self._unpack_env_vars(relayd_env_vars):
817 self._log("{}={}".format(name, value))
818 relayd_env[name] = value
819
11ababae
KS
820 if self.lttng_home_location is not None:
821 relayd_env["LTTNG_HOME"] = str(self.lttng_home_location)
45ce5eed 822 self._log(
11ababae
KS
823 "Launching relayd with LTTNG_HOME='${}'".format(
824 str(self.lttng_home_location)
825 )
45ce5eed 826 )
55aec41f
KS
827 verbose = []
828 if os.environ.get("LTTNG_TEST_VERBOSE_RELAYD") is not None:
829 verbose = ["-vvv"]
45ce5eed
KS
830 process = subprocess.Popen(
831 [
832 str(relayd_path),
833 "-C",
834 "tcp://0.0.0.0:{}".format(self.lttng_relayd_control_port),
835 "-D",
836 "tcp://0.0.0.0:{}".format(self.lttng_relayd_data_port),
837 "-L",
838 "tcp://localhost:{}".format(self.lttng_relayd_live_port),
55aec41f
KS
839 ]
840 + verbose,
45ce5eed
KS
841 stdout=subprocess.PIPE,
842 stderr=subprocess.STDOUT,
843 env=relayd_env,
844 )
845
846 if self._logging_function:
847 self._relayd_output_consumer = ProcessOutputConsumer(
848 process, "lttng-relayd", self._logging_function
849 )
850 self._relayd_output_consumer.daemon = True
851 self._relayd_output_consumer.start()
852
c1eb72c6
KS
853 if os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD") is not None:
854 subprocess.Popen(
855 [
856 "gdbserver",
857 "--attach",
858 "localhost:{}".format(
859 os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_PORT", "1025")
860 ),
861 str(process.pid),
862 ]
863 )
864
865 if os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_WAIT", ""):
866 input("Waiting for user input. Press `Enter` to continue")
867 else:
868 subprocess.Popen(
869 [
870 "gdb",
871 "--batch-silent",
872 "-ex",
873 "target remote localhost:{}".format(
874 os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_PORT", "1025")
875 ),
876 "-ex",
877 "continue",
878 "-ex",
879 "disconnect",
880 ]
881 )
882
45ce5eed
KS
883 return process
884
ce8470c9
MJ
885 def _launch_lttng_sessiond(self):
886 # type: () -> Optional[subprocess.Popen]
ef945e4d
JG
887 is_64bits_host = sys.maxsize > 2**32
888
889 sessiond_path = (
890 self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
891 )
892 consumerd_path_option_name = "--consumerd{bitness}-path".format(
893 bitness="64" if is_64bits_host else "32"
894 )
895 consumerd_path = (
896 self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
897 )
898
899 no_sessiond_var = os.environ.get("TEST_NO_SESSIOND")
900 if no_sessiond_var and no_sessiond_var == "1":
901 # Run test without a session daemon; the user probably
902 # intends to run one under gdb for example.
903 return None
904
905 # Setup the session daemon's environment
906 sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS")
907 sessiond_env = os.environ.copy()
11ababae 908 sessiond_env.update(self._extra_env_vars)
ef945e4d
JG
909 if sessiond_env_vars:
910 self._log("Additional lttng-sessiond environment variables:")
911 additional_vars = self._unpack_env_vars(sessiond_env_vars)
912 for var_name, var_value in additional_vars:
913 self._log(" {name}={value}".format(name=var_name, value=var_value))
914 sessiond_env[var_name] = var_value
915
916 sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
917 self._project_root / "src" / "common"
918 )
919
11ababae
KS
920 if self.lttng_home_location is not None:
921 sessiond_env["LTTNG_HOME"] = str(self.lttng_home_location)
ef945e4d
JG
922
923 wait_queue = _SignalWaitQueue()
0ac0f70e
JG
924 with wait_queue.intercept_signal(signal.SIGUSR1):
925 self._log(
926 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
11ababae 927 home_dir=str(self.lttng_home_location)
0ac0f70e
JG
928 )
929 )
55aec41f
KS
930 verbose = []
931 if os.environ.get("LTTNG_TEST_VERBOSE_SESSIOND") is not None:
932 verbose = ["-vvv", "--verbose-consumer"]
0ac0f70e
JG
933 process = subprocess.Popen(
934 [
935 str(sessiond_path),
936 consumerd_path_option_name,
937 str(consumerd_path),
938 "--sig-parent",
55aec41f
KS
939 ]
940 + verbose,
0ac0f70e
JG
941 stdout=subprocess.PIPE,
942 stderr=subprocess.STDOUT,
943 env=sessiond_env,
ef945e4d 944 )
ef945e4d 945
0ac0f70e
JG
946 if self._logging_function:
947 self._sessiond_output_consumer = ProcessOutputConsumer(
948 process, "lttng-sessiond", self._logging_function
949 ) # type: Optional[ProcessOutputConsumer]
950 self._sessiond_output_consumer.daemon = True
951 self._sessiond_output_consumer.start()
ef945e4d 952
0ac0f70e
JG
953 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
954 wait_queue.wait_for_signal()
ef945e4d 955
c1eb72c6
KS
956 if os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND") is not None:
957 subprocess.Popen(
958 [
959 "gdbserver",
960 "--attach",
961 "localhost:{}".format(
962 os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_PORT", "1024")
963 ),
964 str(process.pid),
965 ]
966 )
967
968 if os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_WAIT", ""):
969 input("Waiting for user input. Press `Enter` to continue")
970 else:
971 subprocess.Popen(
972 [
973 "gdb",
974 "--batch-silent",
975 "-ex",
976 "target remote localhost:{}".format(
977 os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_PORT", "1024")
978 ),
979 "-ex",
980 "continue",
981 "-ex",
982 "disconnect",
983 ]
984 )
985
ef945e4d
JG
986 return process
987
ce8470c9
MJ
988 def _handle_termination_signal(self, signal_number, frame):
989 # type: (int, Optional[FrameType]) -> None
ef945e4d
JG
990 self._log(
991 "Killed by {signal_name} signal, cleaning-up".format(
992 signal_name=signal.strsignal(signal_number)
993 )
994 )
995 self._cleanup()
996
91118dcc
KS
997 def launch_live_viewer(self, session, hostname=None):
998 # Make sure the relayd is ready
999 ready = False
1000 ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"]
1001 query_executor = bt2.QueryExecutor(
1002 ctf_live_cc,
1003 "sessions",
1004 params={"url": "net://localhost:{}".format(self.lttng_relayd_live_port)},
1005 )
1006 while not ready:
1007 try:
1008 query_result = query_executor.query()
1009 except bt2._Error:
1010 time.sleep(0.1)
1011 continue
1012 for live_session in query_result:
1013 if live_session["session-name"] == session:
1014 ready = True
1015 self._log(
1016 "Session '{}' is available at net://localhost:{}".format(
1017 session, self.lttng_relayd_live_port
1018 )
1019 )
1020 break
1021 return _LiveViewer(self, session, hostname)
1022
c661f2f4
JG
1023 def launch_wait_trace_test_application(
1024 self,
1025 event_count, # type: int
1026 wait_time_between_events_us=0,
1027 wait_before_exit=False,
1028 wait_before_exit_file_path=None,
03775d44 1029 run_as=None,
c661f2f4 1030 ):
03775d44 1031 # type: (int, int, bool, Optional[pathlib.Path], Optional[str]) -> _WaitTraceTestApplication
ef945e4d
JG
1032 """
1033 Launch an application that will wait before tracing `event_count` events.
1034 """
c661f2f4 1035 return _WaitTraceTestApplication(
ef945e4d
JG
1036 self._project_root
1037 / "tests"
1038 / "utils"
1039 / "testapp"
ef07b7ae
JG
1040 / "gen-ust-events"
1041 / "gen-ust-events",
ef945e4d
JG
1042 event_count,
1043 self,
c661f2f4
JG
1044 wait_time_between_events_us,
1045 wait_before_exit,
1046 wait_before_exit_file_path,
03775d44 1047 run_as,
ef945e4d
JG
1048 )
1049
09a872ef 1050 def launch_test_application(self, subpath):
873d3601 1051 # type () -> TraceTestApplication
da1e97c9
MD
1052 """
1053 Launch an application that will trace from within constructors.
1054 """
c661f2f4 1055 return _TraceTestApplication(
9a28bc04 1056 self._project_root / "tests" / "utils" / "testapp" / subpath,
da1e97c9
MD
1057 self,
1058 )
1059
9a28bc04
KS
1060 def _terminate_relayd(self):
1061 if self._relayd and self._relayd.poll() is None:
1062 self._relayd.terminate()
1063 self._relayd.wait()
1064 if self._relayd_output_consumer:
1065 self._relayd_output_consumer.join()
1066 self._relayd_output_consumer = None
1067 self._log("Relayd killed")
1068 self._relayd = None
1069
ef945e4d 1070 # Clean-up managed processes
ce8470c9
MJ
1071 def _cleanup(self):
1072 # type: () -> None
ef945e4d
JG
1073 if self._sessiond and self._sessiond.poll() is None:
1074 # The session daemon is alive; kill it.
1075 self._log(
1076 "Killing session daemon (pid = {sessiond_pid})".format(
1077 sessiond_pid=self._sessiond.pid
1078 )
1079 )
1080
1081 self._sessiond.terminate()
1082 self._sessiond.wait()
1083 if self._sessiond_output_consumer:
1084 self._sessiond_output_consumer.join()
1085 self._sessiond_output_consumer = None
1086
1087 self._log("Session daemon killed")
1088 self._sessiond = None
1089
9a28bc04 1090 self._terminate_relayd()
45ce5eed 1091
03775d44
KS
1092 # The user accounts will always be deleted, but the home directories will
1093 # be retained unless the user has opted to preserve the test environment.
1094 userdel = ["userdel"]
1095 if not self.preserve_test_env:
1096 userdel += ["--remove"]
1097 for uid, name in self._dummy_users.items():
1098 # When subprocess is run during the interpreter teardown, ImportError
1099 # may be raised; however, the commands seem to execute correctly.
1100 # Eg.
1101 #
1102 # Exception ignored in: <function _Environment.__del__ at 0x7f2d62e3b9c0>
1103 # Traceback (most recent call last):
1104 # File "tests/utils/lttngtest/environment.py", line 1024, in __del__
1105 # File "tests/utils/lttngtest/environment.py", line 1016, in _cleanup
1106 # File "/usr/lib/python3.11/subprocess.py", line 1026, in __init__
1107 # File "/usr/lib/python3.11/subprocess.py", line 1880, in _execute_child
1108 # File "<frozen os>", line 629, in get_exec_path
1109 # ImportError: sys.meta_path is None, Python is likely shutting down
1110 #
1111 try:
1112 _proc = subprocess.Popen(
1113 ["pkill", "--uid", str(uid)], stderr=subprocess.PIPE
1114 )
1115 _proc.wait()
1116 except ImportError:
1117 pass
1118 try:
1119 _proc = subprocess.Popen(userdel + [name], stderr=subprocess.PIPE)
1120 _proc.wait()
1121 except ImportError:
1122 pass
1123
ef945e4d
JG
1124 self._lttng_home = None
1125
1126 def __del__(self):
1127 self._cleanup()
1128
1129
1130@contextlib.contextmanager
11ababae
KS
1131def test_environment(
1132 with_sessiond,
1133 log=None,
1134 with_relayd=False,
1135 extra_env_vars=dict(),
1136 skip_temporary_lttng_home=False,
1137):
45ce5eed 1138 # type: (bool, Optional[Callable[[str], None]], bool) -> Iterator[_Environment]
11ababae
KS
1139 env = _Environment(
1140 with_sessiond, log, with_relayd, extra_env_vars, skip_temporary_lttng_home
1141 )
ef945e4d
JG
1142 try:
1143 yield env
1144 finally:
1145 env._cleanup()
This page took 0.087653 seconds and 4 git commands to generate.