The Gaudi Framework  v41r0 (bdbe7744)
Loading...
Searching...
No Matches
GaudiTesting.SubprocessBaseTest.SubprocessBaseTest Class Reference
Inheritance diagram for GaudiTesting.SubprocessBaseTest.SubprocessBaseTest:

Public Member Functions

Path|None cwd (self)
str resolve_path (cls, Path|str path)
None update_env (cls, dict[str, str] env)
FixtureResult run_program (cls, tmp_path=None)
 run_program_for_dbg (cls, tmp_path)
None test_fixture_setup (self, Callable[[str, str], None] record_property, FixtureResult fixture_result, Path|None reference_path)
None test_returncode (self, int returncode)

Static Public Member Functions

str expand_vars_from (str value, dict[str, str] env)
None unset_vars (dict[str, str] env, list[str] vars_to_unset)

Static Public Attributes

list command = None
str reference = None
list environment = None
int timeout = 600
int returncode = 0
dict popen_kwargs = {}
list env_to_record

Protected Member Functions

dict[str, str] _prepare_environment (cls)
str _determine_program (cls, str prog)
list[str] _prepare_command (cls, tmp_path=Path())
str _handle_timeout (cls, subprocess.Popen proc)
 _prepare_execution (cls, tmp_path=None)

Static Protected Member Functions

str _collect_stack_trace (subprocess.Popen proc)
None _terminate_process (subprocess.Popen proc)

Detailed Description

A base class for running and managing subprocess executions within a test framework.
It provides mechanisms for setting up the environment, preparing commands,
and handling subprocess output and errors.

Definition at line 35 of file SubprocessBaseTest.py.

Member Function Documentation

◆ _collect_stack_trace()

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._collect_stack_trace ( subprocess.Popen proc)
staticprotected
Collect stack trace from a running process using the available debugger.
Uses lldb on macOS and gdb on Linux. Returns a message if no debugger
is available.

Definition at line 134 of file SubprocessBaseTest.py.

134 def _collect_stack_trace(proc: subprocess.Popen) -> str:
135 """
136 Collect stack trace from a running process using the available debugger.
137 Uses lldb on macOS and gdb on Linux. Returns a message if no debugger
138 is available.
139 """
140 import sys
141
142 if sys.platform == "darwin":
143 # Use lldb on macOS
144 if not shutil.which("lldb"):
145 return "(lldb not available for stack trace collection)"
146 cmd = [
147 "lldb",
148 "-p",
149 str(proc.pid),
150 "-o",
151 "bt all",
152 "-o",
153 "quit",
154 ]
155 else:
156 # Use gdb on Linux
157 if not shutil.which("gdb"):
158 return "(gdb not available for stack trace collection)"
159 cmd = [
160 "gdb",
161 "--pid",
162 str(proc.pid),
163 "--batch",
164 "--eval-command=thread apply all backtrace",
165 ]
166 debugger = subprocess.Popen(
167 cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
168 )
169 return debugger.communicate()[0].decode("utf-8", errors="backslashreplace")
170

◆ _determine_program()

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._determine_program ( cls,
str prog )
protected

Definition at line 106 of file SubprocessBaseTest.py.

106 def _determine_program(cls, prog: str) -> str:
107 if not any(prog.lower().endswith(ext) for ext in [".exe", ".py", ".bat"]):
108 prog += ".exe"
109 return which(prog) or cls.resolve_path(prog)
110

◆ _handle_timeout()

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._handle_timeout ( cls,
subprocess.Popen proc )
protected
Handle a process timeout by collecting and returning the stack trace.

Definition at line 125 of file SubprocessBaseTest.py.

125 def _handle_timeout(cls, proc: subprocess.Popen) -> str:
126 """
127 Handle a process timeout by collecting and returning the stack trace.
128 """
129 stack_trace = cls._collect_stack_trace(proc)
130 cls._terminate_process(proc)
131 return stack_trace
132

◆ _prepare_command()

list[str] GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._prepare_command ( cls,
tmp_path = Path() )
protected
Prepare the command to be executed, resolving paths for each part.

Reimplemented in GaudiTesting.GaudiExeTest.GaudiExeTest.

Definition at line 112 of file SubprocessBaseTest.py.

112 def _prepare_command(cls, tmp_path=Path()) -> list[str]:
113 """
114 Prepare the command to be executed, resolving paths for each part.
115 """
116 command = [cls._determine_program(cls.command[0])]
117 for part in cls.command[1:]:
118 if not part.startswith("-"): # do not try to expand options
119 command.append(cls.resolve_path(part))
120 else:
121 command.append(part)
122 return command
123

◆ _prepare_environment()

dict[str, str] GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._prepare_environment ( cls)
protected

Definition at line 91 of file SubprocessBaseTest.py.

91 def _prepare_environment(cls) -> dict[str, str]:
92 env = dict(os.environ)
93 cls.update_env(env)
94 return env
95

◆ _prepare_execution()

GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._prepare_execution ( cls,
tmp_path = None )
protected

Definition at line 179 of file SubprocessBaseTest.py.

179 def _prepare_execution(cls, tmp_path=None):
180 command = cls._prepare_command(tmp_path=tmp_path)
181 env = cls._prepare_environment()
182 os.makedirs(cls.popen_kwargs.get("cwd", tmp_path), exist_ok=True)
183 return command, env
184

◆ _terminate_process()

None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest._terminate_process ( subprocess.Popen proc)
staticprotected

Definition at line 172 of file SubprocessBaseTest.py.

172 def _terminate_process(proc: subprocess.Popen) -> None:
173 kill_tree(proc.pid, signal.SIGTERM)
174 proc.wait(60)
175 if proc.poll() is None:
176 kill_tree(proc.pid, signal.SIGKILL)
177

◆ cwd()

Path | None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.cwd ( self)

Definition at line 54 of file SubprocessBaseTest.py.

54 def cwd(self) -> Path | None:
55 cwd = self.popen_kwargs.get("cwd")
56 return Path(cwd) if cwd else None
57

◆ expand_vars_from()

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.expand_vars_from ( str value,
dict[str, str] env )
static

Definition at line 97 of file SubprocessBaseTest.py.

97 def expand_vars_from(value: str, env: dict[str, str]) -> str:
98 return Template(value).safe_substitute(env)
99

◆ resolve_path()

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.resolve_path ( cls,
Path | str path )
Resolve the given path to an absolute path,
expanding environment variables.
If path looks relative and does not point to anything
it is not modified.

Definition at line 59 of file SubprocessBaseTest.py.

59 def resolve_path(cls, path: Path | str) -> str:
60 """
61 Resolve the given path to an absolute path,
62 expanding environment variables.
63 If path looks relative and does not point to anything
64 it is not modified.
65 """
66 if isinstance(path, Path):
67 path = str(path)
68 path = os.path.expandvars(path)
69
70 # handle the special case "path/to/file:some_suffix"
71 suffix = ""
72 if ":" in path:
73 path, suffix = path.rsplit(":", 1)
74 suffix = f":{suffix}"
75
76 if not os.path.isabs(path):
77 base_dir = file_path_for_class(cls).parent
78 possible_path = str((base_dir / path).resolve())
79 if os.path.exists(possible_path):
80 path = possible_path
81 return path + suffix
82

◆ run_program()

FixtureResult GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.run_program ( cls,
tmp_path = None )
Run the specified program and capture its output.

Definition at line 186 of file SubprocessBaseTest.py.

186 def run_program(cls, tmp_path=None) -> FixtureResult:
187 """
188 Run the specified program and capture its output.
189 """
190 start_time = datetime.now()
191 command, env = cls._prepare_execution(tmp_path=tmp_path)
192
193 proc = subprocess.Popen(
194 command,
195 stdout=subprocess.PIPE,
196 stderr=subprocess.PIPE,
197 env=env,
198 **cls.popen_kwargs,
199 )
200
201 stdout_chunks, stderr_chunks = [], []
202 stdout = stderr = ""
203 exceeded_stream = stack_trace = run_exception = None
204 streams = {
205 proc.stdout.fileno(): (stdout_chunks, "stdout"),
206 proc.stderr.fileno(): (stderr_chunks, "stderr"),
207 }
208
209 def read_output():
210 nonlocal stdout, stderr, exceeded_stream
211 while not exceeded_stream and proc.poll() is None:
212 readable, _, _ = select.select(streams.keys(), [], [], cls.timeout)
213 for fileno in readable:
214 data = os.read(fileno, 1024)
215 chunks, stream_name = streams[fileno]
216 chunks.append(data)
217 if sum(len(chunk) for chunk in chunks) > STDOUT_LIMIT:
218 exceeded_stream = stream_name
219 break
220
221 stdout = b"".join(stdout_chunks)
222 stderr = b"".join(stderr_chunks)
223
224 thread = threading.Thread(target=read_output)
225 thread.start()
226 thread.join(cls.timeout)
227
228 if thread.is_alive():
229 stack_trace = cls._handle_timeout(proc)
230 run_exception = ProcessTimeoutError("Process timed out", stack_trace)
231 elif exceeded_stream:
232 run_exception = ExceededStreamError(
233 "Stream exceeded size limit", exceeded_stream
234 )
235
236 end_time = datetime.now()
237
238 completed_process = subprocess.CompletedProcess(
239 args=command,
240 returncode=proc.returncode,
241 stdout=stdout,
242 stderr=stderr,
243 )
244
245 # Only record environment variables that match at least one
246 # of the regular expressions in env_to_record.
247 # If env_to_record is empty or None, do not record any env variable
248 env_to_record = list(cls.env_to_record) if cls.env_to_record else []
249 # extend env_to_record with the regular expressions in environment variables
250 # named GAUDI_ENV_TO_RECORD, GAUDI_ENV_TO_RECORD_1, GAUDI_ENV_TO_RECORD_2, ...
251 env_to_record.extend(
252 env[name] for name in env if re.match(r"GAUDI_ENV_TO_RECORD(_\d+)?", name)
253 )
254 env = {
255 key: value
256 for key, value in env.items()
257 if any(re.match(exp, key) for exp in env_to_record)
258 }
259 return FixtureResult(
260 completed_process=completed_process,
261 start_time=start_time,
262 end_time=end_time,
263 run_exception=run_exception,
264 command=cls.command,
265 expanded_command=command,
266 env=env,
267 cwd=cls.popen_kwargs["cwd"],
268 )
269

◆ run_program_for_dbg()

GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.run_program_for_dbg ( cls,
tmp_path )

Definition at line 271 of file SubprocessBaseTest.py.

271 def run_program_for_dbg(cls, tmp_path):
272 tmp_path = cls.popen_kwargs.get("cwd", tmp_path)
273 command, env = cls._prepare_execution(tmp_path=tmp_path)
274 print("Running the command: ", command)
275 subprocess.run(
276 command,
277 env=env,
278 cwd=tmp_path,
279 **cls.popen_kwargs,
280 )
281

◆ test_fixture_setup()

None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.test_fixture_setup ( self,
Callable[[str, str], None] record_property,
FixtureResult fixture_result,
Path | None reference_path )
Record properties and handle any failures during fixture setup.

Definition at line 283 of file SubprocessBaseTest.py.

288 ) -> None:
289 """
290 Record properties and handle any failures during fixture setup.
291 """
292 for key, value in fixture_result.to_dict().items():
293 if value is not None:
294 record_property(key, value)
295 if reference_path:
296 record_property("reference_file", str(reference_path))
297
298 if fixture_result.run_exception:
299 pytest.fail(f"{fixture_result.run_exception}")
300

◆ test_returncode()

None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.test_returncode ( self,
int returncode )
Test that the return code matches the expected value.

Definition at line 302 of file SubprocessBaseTest.py.

302 def test_returncode(self, returncode: int) -> None:
303 """
304 Test that the return code matches the expected value.
305 """
306 assert returncode == self.returncode

◆ unset_vars()

None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.unset_vars ( dict[str, str] env,
list[str] vars_to_unset )
static

Definition at line 101 of file SubprocessBaseTest.py.

101 def unset_vars(env: dict[str, str], vars_to_unset: list[str]) -> None:
102 for var in vars_to_unset:
103 env.pop(var, None)
104

◆ update_env()

None GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.update_env ( cls,
dict[str, str] env )

Definition at line 84 of file SubprocessBaseTest.py.

84 def update_env(cls, env: dict[str, str]) -> None:
85 if cls.environment:
86 for item in cls.environment:
87 key, value = item.split("=", 1)
88 env[key] = cls.expand_vars_from(value, env)
89

Member Data Documentation

◆ command

list GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.command = None
static

Definition at line 42 of file SubprocessBaseTest.py.

◆ env_to_record

list GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.env_to_record
static
Initial value:
= [
r"^(.*PATH|BINARY_TAG|CMTCONFIG|PWD|TMPDIR|DISPLAY|LC_[A-Z]+|LANGUAGE|.*ROOT|.*OPTS|GAUDI_ENV_TO_RECORD(_\d+)?)$"
]

Definition at line 49 of file SubprocessBaseTest.py.

◆ environment

list GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.environment = None
static

Definition at line 44 of file SubprocessBaseTest.py.

◆ popen_kwargs

GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.popen_kwargs = {}
static

Definition at line 47 of file SubprocessBaseTest.py.

◆ reference

str GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.reference = None
static

Definition at line 43 of file SubprocessBaseTest.py.

◆ returncode

int GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.returncode = 0
static

Definition at line 46 of file SubprocessBaseTest.py.

◆ timeout

GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.timeout = 600
static

Definition at line 45 of file SubprocessBaseTest.py.


The documentation for this class was generated from the following file: