3 # Copyright (C) 2022 Jérémie Galarneau <jeremie.galarneau@efficios.com>
5 # SPDX-License-Identifier: GPL-2.0-only
8 from types
import FrameType
9 from typing
import Callable
, Iterator
, Optional
, Tuple
, List
25 class TemporaryDirectory
:
26 def __init__(self
, prefix
):
28 self
._directory
_path
= tempfile
.mkdtemp(prefix
=prefix
)
31 shutil
.rmtree(self
._directory
_path
, ignore_errors
=True)
35 # type: () -> pathlib.Path
36 return pathlib
.Path(self
._directory
_path
)
39 class _SignalWaitQueue
:
41 Utility class useful to wait for a signal before proceeding.
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.
47 signal.signal(signal.SIGWHATEVER, queue.signal)
49 Waiting for the signal:
50 queue.wait_for_signal()
54 self
._queue
= queue
.Queue() # type: queue.Queue
59 frame
, # type: Optional[FrameType]
61 self
._queue
.put_nowait(signal_number
)
63 def wait_for_signal(self
):
64 self
._queue
.get(block
=True)
67 class WaitTraceTestApplication
:
69 Create an application that waits before tracing. This allows a test to
70 launch an application, get its PID, and get it to start tracing when it
71 has completed its setup.
76 binary_path
, # type: pathlib.Path
77 event_count
, # type: int
78 environment
, # type: Environment
79 wait_time_between_events_us
=0, # type: int
81 self
._environment
= environment
# type: Environment
83 # The test application currently produces 5 different events per iteration.
84 raise ValueError("event count must be a multiple of 5")
85 self
._iteration
_count
= int(event_count
/ 5) # type: int
86 # File that the application will wait to see before tracing its events.
87 self
._app
_start
_tracing
_file
_path
= pathlib
.Path(
90 suffix
="_start_tracing",
91 dir=self
._compat
_open
_path
(environment
.lttng_home_location
),
94 self
._has
_returned
= False
96 test_app_env
= os
.environ
.copy()
97 test_app_env
["LTTNG_HOME"] = str(environment
.lttng_home_location
)
98 # Make sure the app is blocked until it is properly registered to
100 test_app_env
["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
102 # File that the application will create to indicate it has completed its initialization.
103 app_ready_file_path
= tempfile
.mktemp(
106 dir=self
._compat
_open
_path
(environment
.lttng_home_location
),
109 test_app_args
= [str(binary_path
)]
110 test_app_args
.extend(
112 "--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(
113 iteration_count
=self
._iteration
_count
,
114 app_ready_file_path
=app_ready_file_path
,
115 app_start_tracing_file_path
=self
._app
_start
_tracing
_file
_path
,
116 wait_time_between_events_us
=wait_time_between_events_us
,
121 self
._process
= subprocess
.Popen(
124 ) # type: subprocess.Popen
126 # Wait for the application to create the file indicating it has fully
127 # initialized. Make sure the app hasn't crashed in order to not wait
130 if os
.path
.exists(app_ready_file_path
):
133 if self
._process
.poll() is not None:
134 # Application has unexepectedly returned.
136 "Test application has unexepectedly returned during its initialization with return code `{return_code}`".format(
137 return_code
=self
._process
.returncode
145 if self
._process
.poll() is not None:
146 # Application has unexepectedly returned.
148 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
149 return_code
=self
._process
.returncode
152 open(self
._compat
_open
_path
(self
._app
_start
_tracing
_file
_path
), mode
="x")
154 def wait_for_exit(self
):
156 if self
._process
.wait() != 0:
158 "Test application has exit with return code `{return_code}`".format(
159 return_code
=self
._process
.returncode
162 self
._has
_returned
= True
167 return self
._process
.pid
170 def _compat_open_path(path
):
171 # type: (pathlib.Path) -> pathlib.Path | str
173 The builtin open() in python >= 3.6 expects a path-like object while
174 prior versions expect a string or bytes object. Return the correct type
175 based on the presence of the "__fspath__" attribute specified in PEP-519.
177 if hasattr(path
, "__fspath__"):
183 if not self
._has
_returned
:
184 # This is potentially racy if the pid has been recycled. However,
185 # we can't use pidfd_open since it is only available in python >= 3.9.
190 class TraceTestApplication
:
192 Create an application that emits events as soon as it is launched. In most
193 scenarios, it is preferable to use a WaitTraceTestApplication.
196 def __init__(self
, binary_path
, environment
):
197 # type: (pathlib.Path, Environment)
198 self
._environment
= environment
# type: Environment
199 self
._has
_returned
= False
201 test_app_env
= os
.environ
.copy()
202 test_app_env
["LTTNG_HOME"] = str(environment
.lttng_home_location
)
203 # Make sure the app is blocked until it is properly registered to
204 # the session daemon.
205 test_app_env
["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
207 test_app_args
= [str(binary_path
)]
209 self
._process
: subprocess
.Popen
= subprocess
.Popen(
210 test_app_args
, env
=test_app_env
213 def wait_for_exit(self
):
215 if self
._process
.wait() != 0:
217 "Test application has exit with return code `{return_code}`".format(
218 return_code
=self
._process
.returncode
221 self
._has
_returned
= True
224 if not self
._has
_returned
:
225 # This is potentially racy if the pid has been recycled. However,
226 # we can't use pidfd_open since it is only available in python >= 3.9.
231 class ProcessOutputConsumer(threading
.Thread
, logger
._Logger
):
234 process
, # type: subprocess.Popen
236 log
, # type: Callable[[str], None]
238 threading
.Thread
.__init
__(self
)
240 logger
._Logger
.__init
__(self
, log
)
241 self
._process
= process
245 while self
._process
.poll() is None:
246 assert self
._process
.stdout
247 line
= self
._process
.stdout
.readline().decode("utf-8").replace("\n", "")
249 self
._log
("{prefix}: {line}".format(prefix
=self
._prefix
, line
=line
))
252 # Generate a temporary environment in which to execute a test.
253 class _Environment(logger
._Logger
):
256 with_sessiond
, # type: bool
257 log
=None, # type: Optional[Callable[[str], None]]
259 super().__init
__(log
)
260 signal
.signal(signal
.SIGTERM
, self
._handle
_termination
_signal
)
261 signal
.signal(signal
.SIGINT
, self
._handle
_termination
_signal
)
263 # Assumes the project's hierarchy to this file is:
264 # tests/utils/python/this_file
265 self
._project
_root
= (
266 pathlib
.Path(__file__
).absolute().parents
[3]
267 ) # type: pathlib.Path
268 self
._lttng
_home
= TemporaryDirectory(
269 "lttng_test_env_home"
270 ) # type: Optional[TemporaryDirectory]
273 self
._launch
_lttng
_sessiond
() if with_sessiond
else None
274 ) # type: Optional[subprocess.Popen[bytes]]
277 def lttng_home_location(self
):
278 # type: () -> pathlib.Path
279 if self
._lttng
_home
is None:
280 raise RuntimeError("Attempt to access LTTng home after clean-up")
281 return self
._lttng
_home
.path
284 def lttng_client_path(self
):
285 # type: () -> pathlib.Path
286 return self
._project
_root
/ "src" / "bin" / "lttng" / "lttng"
288 def create_temporary_directory(self
, prefix
=None):
289 # type: (Optional[str]) -> pathlib.Path
290 # Simply return a path that is contained within LTTNG_HOME; it will
291 # be destroyed when the temporary home goes out of scope.
292 assert self
._lttng
_home
295 prefix
="tmp" if prefix
is None else prefix
,
296 dir=str(self
._lttng
_home
.path
),
300 # Unpack a list of environment variables from a string
301 # such as "HELLO=is_it ME='/you/are/looking/for'"
303 def _unpack_env_vars(env_vars_string
):
304 # type: (str) -> List[Tuple[str, str]]
306 for var
in shlex
.split(env_vars_string
):
307 equal_position
= var
.find("=")
308 # Must have an equal sign and not end with an equal sign
309 if equal_position
== -1 or equal_position
== len(var
) - 1:
311 "Invalid sessiond environment variable: `{}`".format(var
)
314 var_name
= var
[0:equal_position
]
315 var_value
= var
[equal_position
+ 1 :]
317 var_value
= var_value
.replace("'", "")
318 var_value
= var_value
.replace('"', "")
319 unpacked_vars
.append((var_name
, var_value
))
323 def _launch_lttng_sessiond(self
):
324 # type: () -> Optional[subprocess.Popen]
325 is_64bits_host
= sys
.maxsize
> 2**32
328 self
._project
_root
/ "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
330 consumerd_path_option_name
= "--consumerd{bitness}-path".format(
331 bitness
="64" if is_64bits_host
else "32"
334 self
._project
_root
/ "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
337 no_sessiond_var
= os
.environ
.get("TEST_NO_SESSIOND")
338 if no_sessiond_var
and no_sessiond_var
== "1":
339 # Run test without a session daemon; the user probably
340 # intends to run one under gdb for example.
343 # Setup the session daemon's environment
344 sessiond_env_vars
= os
.environ
.get("LTTNG_SESSIOND_ENV_VARS")
345 sessiond_env
= os
.environ
.copy()
346 if sessiond_env_vars
:
347 self
._log
("Additional lttng-sessiond environment variables:")
348 additional_vars
= self
._unpack
_env
_vars
(sessiond_env_vars
)
349 for var_name
, var_value
in additional_vars
:
350 self
._log
(" {name}={value}".format(name
=var_name
, value
=var_value
))
351 sessiond_env
[var_name
] = var_value
353 sessiond_env
["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
354 self
._project
_root
/ "src" / "common"
357 assert self
._lttng
_home
is not None
358 sessiond_env
["LTTNG_HOME"] = str(self
._lttng
_home
.path
)
360 wait_queue
= _SignalWaitQueue()
361 signal
.signal(signal
.SIGUSR1
, wait_queue
.signal
)
364 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
365 home_dir
=str(self
._lttng
_home
.path
)
368 process
= subprocess
.Popen(
371 consumerd_path_option_name
,
375 stdout
=subprocess
.PIPE
,
376 stderr
=subprocess
.STDOUT
,
380 if self
._logging
_function
:
381 self
._sessiond
_output
_consumer
= ProcessOutputConsumer(
382 process
, "lttng-sessiond", self
._logging
_function
383 ) # type: Optional[ProcessOutputConsumer]
384 self
._sessiond
_output
_consumer
.daemon
= True
385 self
._sessiond
_output
_consumer
.start()
387 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
388 wait_queue
.wait_for_signal()
389 signal
.signal(signal
.SIGUSR1
, wait_queue
.signal
)
393 def _handle_termination_signal(self
, signal_number
, frame
):
394 # type: (int, Optional[FrameType]) -> None
396 "Killed by {signal_name} signal, cleaning-up".format(
397 signal_name
=signal
.strsignal(signal_number
)
402 def launch_wait_trace_test_application(self
, event_count
):
403 # type: (int) -> WaitTraceTestApplication
405 Launch an application that will wait before tracing `event_count` events.
407 return WaitTraceTestApplication(
418 def launch_trace_test_constructor_application(self
):
419 # type () -> TraceTestApplication
421 Launch an application that will trace from within constructors.
423 return TraceTestApplication(
428 / "gen-ust-events-constructor"
429 / "gen-ust-events-constructor",
433 # Clean-up managed processes
436 if self
._sessiond
and self
._sessiond
.poll() is None:
437 # The session daemon is alive; kill it.
439 "Killing session daemon (pid = {sessiond_pid})".format(
440 sessiond_pid
=self
._sessiond
.pid
444 self
._sessiond
.terminate()
445 self
._sessiond
.wait()
446 if self
._sessiond
_output
_consumer
:
447 self
._sessiond
_output
_consumer
.join()
448 self
._sessiond
_output
_consumer
= None
450 self
._log
("Session daemon killed")
451 self
._sessiond
= None
453 self
._lttng
_home
= None
459 @contextlib.contextmanager
460 def test_environment(with_sessiond
, log
=None):
461 # type: (bool, Optional[Callable[[str], None]]) -> Iterator[_Environment]
462 env
= _Environment(with_sessiond
, log
)