Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ test_support Change Log
UNRELEASED
----------

* ADDED: AssertiveComparisonCheckers suppress_multidrive_messages support/param to
* ADDED: AssertiveComparisonChecker's suppress_multidrive_messages support/param to
ComparisonChecker
* ADDED: Methods in Xsi class for getting the xsim tick frequency
* CHANGED: Pyxsim CMake build uses XCommon CMake
* CHANGED: The way time is incremented by time_step for better floating point precision
* FIXED: Resolved issues with stdout/stderr capture in Pyxsim
* FIXED: Subprocess exit code checking in Pyxsim to properly report errors from
failed commands

2.0.0
-----
Expand Down
16 changes: 15 additions & 1 deletion lib/python/Pyxsim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ def run_on_simulator_(xe, tester=None, simthreads=[], **kwargs):
if not build_success:
return False

run_with_pyxsim(xe, simthreads, **kwargs)
sim_success = run_with_pyxsim(xe, simthreads, **kwargs)

if not sim_success:
return False

if tester and capfd:
cap_output, err = capfd.readouterr()
Expand Down Expand Up @@ -216,6 +219,17 @@ def run_with_pyxsim(
if p.is_alive():
sys.stderr.write("Simulator timed out\n")
p.terminate()
Comment thread
xross marked this conversation as resolved.
p.join(timeout=1)
if p.is_alive() and hasattr(p, "kill"):
p.kill()
p.join()
return False
Comment thread
xross marked this conversation as resolved.

if p.exitcode != 0:
sys.stderr.write(f"Simulator process failed with exit code {p.exitcode}\n")
return False

return True


class SimThread:
Expand Down
57 changes: 41 additions & 16 deletions lib/python/Pyxsim/xmostest_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def Popen(*args, **kwargs):


def wait_with_timeout(p_and_sig, timeout):
(ev, _pidv, process) = p_and_sig
(ev, _pidv, retv, process) = p_and_sig
process.start()
finished = True
try:
Expand All @@ -124,11 +124,13 @@ def wait_with_timeout(p_and_sig, timeout):
ev.wait()
except KeyboardInterrupt:
pstreekill(process)
Comment thread
xross marked this conversation as resolved.
finally:
process.join()

Comment on lines 124 to 129
return not finished
return (not finished, retv.value)


def do_cmd(ev, pidv, *args, **kwargs):
def do_cmd(ev, pidv, retv, *args, **kwargs):
if not platform_is_windows():
os.setpgid(os.getpid(), 0)
if "stdout_fname" in kwargs:
Expand All @@ -140,21 +142,22 @@ def do_cmd(ev, pidv, *args, **kwargs):
process = Popen(*args, **kwargs)
pidv.value = process.pid
try:
process.wait()
retv.value = process.wait()
except KeyboardInterrupt:
# Catch the KeyboardInterrupt raised due to the SIGINT signal
# sent by pstreekill()
pass
retv.value = -1
ev.set()


def create_cmd_process(*args, **kwargs):
ev = multiprocessing.Event()
pidv = multiprocessing.Value("d", 0)
args = tuple([ev, pidv] + list(args))
retv = multiprocessing.Value("i", 0)
args = tuple([ev, pidv, retv] + list(args))
process = multiprocessing.Process(target=do_cmd, args=args, kwargs=kwargs)

return (ev, pidv, process)
return (ev, pidv, retv, process)


def remove(name):
Expand All @@ -177,21 +180,25 @@ def call(*args, **kwargs):
If silent, then create temporary files to pass stdout and stderr to since
on Windows the less/more-like behaviour waits for a keypress if it goes
to stdout.

Raises TestError if the command returns a non-zero exit code.
"""
silent = kwargs.pop("silent", False)
retval = 0
timeout = None
if "timeout" in kwargs:
timeout = kwargs["timeout"]
kwargs.pop("timeout")

cmd_str = " ".join(args[0]) if args else "<unknown command>"
stdout_lines = []

if silent:
out = tempfile.NamedTemporaryFile(delete=False)
kwargs["stdout_fname"] = out.name
kwargs["stderr"] = subprocess.STDOUT

process = create_cmd_process(*args, **kwargs)
timed_out = wait_with_timeout(process, timeout)
timed_out, retval = wait_with_timeout(process, timeout)
out.seek(0)
stdout_lines = out.readlines()
out.close()
Expand All @@ -201,14 +208,20 @@ def call(*args, **kwargs):
log_debug(" " + line.rstrip())
else:
process = create_cmd_process(*args, **kwargs)
timed_out = wait_with_timeout(process, timeout)
timed_out, retval = wait_with_timeout(process, timeout)
# Ensure spawned processes are not left running past this point
# There should be no children running now (as they would be orphaned)
process[2].terminate()
process[2].join(timeout=0.1) # Avoid always printing wait message
while process[2].is_alive():
sys.stdout.write("Waiting for PID %d to terminate\n" % process[2].pid)
process[2].join(timeout=1.0)
process[3].terminate()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe my lack of understanding, but what is the significance of process[3], rather than process.

Copy link
Copy Markdown
Contributor Author

@xross xross May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process is the entire tuple returned by create_cmd_process
the process object is at index 3 :)

https://github.com/xross/test_support/blob/983acc53685e2fc37c1b9aed517eb03a2d32eccb/lib/python/Pyxsim/xmostest_subprocess.py#L160

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could have been cleaner with a class..

process[3].join(timeout=0.1) # Avoid always printing wait message
while process[3].is_alive():
sys.stdout.write("Waiting for PID %d to terminate\n" % process[3].pid)
process[3].join(timeout=1.0)

if retval != 0:
output = "".join(line.decode("utf-8") if isinstance(line, bytes) else line for line in stdout_lines)
raise TestError(
f"Command failed with exit code {retval}: {cmd_str}\nOutput:\n{output}"
)
Comment on lines +220 to +224
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems low risk.

Comment on lines +220 to +224

if timeout:
return (timed_out, retval)
Expand All @@ -219,6 +232,8 @@ def call(*args, **kwargs):
def call_get_output(*args, **kwargs):
"""Create temporary files to pass stdout and stderr to since on Windows the
less/more-like behaviour waits for a keypress if it goes to stdout.

Raises TestError if the command returns a non-zero exit code.
"""
merge = kwargs.pop("merge_out_and_err", False)

Expand All @@ -229,6 +244,9 @@ def call_get_output(*args, **kwargs):
timeout = kwargs["timeout"]
kwargs.pop("timeout")

cmd_str = " ".join(args[0]) if args else "<unknown command>"
stderr_lines = []

if merge:
kwargs["stderr"] = subprocess.STDOUT
else:
Expand All @@ -238,7 +256,7 @@ def call_get_output(*args, **kwargs):
err.close()

process = create_cmd_process(*args, **kwargs)
timed_out = wait_with_timeout(process, timeout)
timed_out, retval = wait_with_timeout(process, timeout)
out.seek(0)
stdout_lines = out.readlines()
out.close()
Expand All @@ -257,6 +275,13 @@ def call_get_output(*args, **kwargs):
line = line.decode("utf-8")
log_debug(" err:" + line.rstrip())

if retval != 0:
stdout_str = "".join(line.decode("utf-8") if isinstance(line, bytes) else line for line in stdout_lines)
stderr_str = "".join(line.decode("utf-8") if isinstance(line, bytes) else line for line in stderr_lines)
Comment on lines +279 to +280
raise TestError(
f"Command failed with exit code {retval}: {cmd_str}\nStdout:\n{stdout_str}\nStderr:\n{stderr_str}"
Comment on lines +278 to +282
Copy link
Copy Markdown
Contributor Author

@xross xross May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems low risk?

)

if merge:
if timeout:
return (timed_out, stdout_lines)
Expand Down