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
246
247
248 env_to_record = list(cls.env_to_record) if cls.env_to_record else []
249
250
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