17 from datetime
import datetime
18 from pathlib
import Path
19 from string
import Template
20 from typing
import Callable, Dict, List, Optional, Union
31 STDOUT_LIMIT = int(os.environ.get(
"GAUDI_TEST_STDOUT_LIMIT", 1024**2 * 100))
36 A base class for running and managing subprocess executions within a test framework.
37 It provides mechanisms for setting up the environment, preparing commands,
38 and handling subprocess output and errors.
41 command: List[str] =
None
43 environment: List[str] =
None
46 popen_kwargs: Dict = {}
48 env_to_record: List[str] = [
49 r"^(.*PATH|BINARY_TAG|CMTCONFIG|PWD|TMPDIR|DISPLAY|LC_[A-Z]+|LANGUAGE|.*ROOT|.*OPTS|GAUDI_ENV_TO_RECORD(_\d+)?)$"
53 def cwd(self) -> Optional[Path]:
54 cwd = self.popen_kwargs.
get(
"cwd")
55 return Path(cwd)
if cwd
else None
60 Resolve the given path to an absolute path,
61 expanding environment variables.
62 If path looks relative and does not point to anything
65 if isinstance(path, Path):
67 path = os.path.expandvars(path)
72 path, suffix = path.rsplit(
":", 1)
75 if not os.path.isabs(path):
77 possible_path =
str((base_dir / path).resolve())
78 if os.path.exists(possible_path):
85 for item
in cls.environment:
86 key, value = item.split(
"=", 1)
91 env = dict(os.environ)
97 return Template(value).safe_substitute(env)
100 def unset_vars(env: Dict[str, str], vars_to_unset: List[str]) ->
None:
101 for var
in vars_to_unset:
106 if not any(prog.lower().endswith(ext)
for ext
in [
".exe",
".py",
".bat"]):
113 Prepare the command to be executed, resolving paths for each part.
116 for part
in cls.command[1:]:
117 if not part.startswith(
"-"):
126 Handle a process timeout by collecting and returning the stack trace.
139 "--eval-command=thread apply all backtrace",
141 gdb = subprocess.Popen(
142 cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
144 return gdb.communicate()[0].decode(
"utf-8", errors=
"backslashreplace")
150 if proc.poll()
is None:
157 os.makedirs(cls.popen_kwargs.
get(
"cwd", tmp_path), exist_ok=
True)
163 Run the specified program and capture its output.
165 start_time = datetime.now()
168 proc = subprocess.Popen(
170 stdout=subprocess.PIPE,
171 stderr=subprocess.PIPE,
176 stdout_chunks, stderr_chunks = [], []
178 exceeded_stream = stack_trace = run_exception =
None
180 proc.stdout.fileno(): (stdout_chunks,
"stdout"),
181 proc.stderr.fileno(): (stderr_chunks,
"stderr"),
185 nonlocal stdout, stderr, exceeded_stream
186 while not exceeded_stream
and proc.poll()
is None:
187 readable, _, _ = select.select(streams.keys(), [], [], cls.timeout)
188 for fileno
in readable:
189 data = os.read(fileno, 1024)
190 chunks, stream_name = streams[fileno]
192 if sum(len(chunk)
for chunk
in chunks) > STDOUT_LIMIT:
193 exceeded_stream = stream_name
196 stdout = b
"".join(stdout_chunks)
197 stderr = b
"".join(stderr_chunks)
199 thread = threading.Thread(target=read_output)
201 thread.join(cls.timeout)
203 if thread.is_alive():
206 elif exceeded_stream:
208 "Stream exceeded size limit", exceeded_stream
211 end_time = datetime.now()
213 completed_process = subprocess.CompletedProcess(
215 returncode=proc.returncode,
223 env_to_record = list(cls.env_to_record)
if cls.env_to_record
else []
226 env_to_record.extend(
227 env[name]
for name
in env
if re.match(
r"GAUDI_ENV_TO_RECORD(_\d+)?", name)
231 for key, value
in env.items()
232 if any(re.match(exp, key)
for exp
in env_to_record)
235 completed_process=completed_process,
236 start_time=start_time,
238 run_exception=run_exception,
240 expanded_command=command,
242 cwd=cls.popen_kwargs[
"cwd"],
247 tmp_path = cls.popen_kwargs.
get(
"cwd", tmp_path)
249 print(
"Running the command: ", command)
257 @pytest.mark.do_not_collect_source
260 record_property: Callable[[str, str],
None],
261 fixture_result: FixtureResult,
262 reference_path: Optional[Path],
265 Record properties and handle any failures during fixture setup.
267 for key, value
in fixture_result.to_dict().items():
268 if value
is not None:
269 record_property(key, value)
271 record_property(
"reference_file",
str(reference_path))
273 if fixture_result.run_exception:
274 pytest.fail(f
"{fixture_result.run_exception}")
276 @pytest.mark.do_not_collect_source
279 Test that the return code matches the expected value.
281 assert returncode == self.returncode