feat(run): capture sub-instance output, add batch param
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from interpreter.test_items.test_result import (TestValue)
|
|||||||
import api.testium as tm
|
import api.testium as tm
|
||||||
from interpreter.utils.constants import TestItemType as cst
|
from interpreter.utils.constants import TestItemType as cst
|
||||||
from interpreter.utils.param_decl import Param, ParamSet
|
from interpreter.utils.param_decl import Param, ParamSet
|
||||||
|
from interpreter.utils.proc_drain import drain_to_log
|
||||||
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
from runtime.tum_except import ETUMSyntaxError, ETUMRuntimeError, item_load_context
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +76,9 @@ class TestItemRun(TestItem):
|
|||||||
Param("wait_for_exec",
|
Param("wait_for_exec",
|
||||||
doc="If true, block until the time window opens. Requires both "
|
doc="If true, block until the time window opens. Requires both "
|
||||||
"start_time and end_time."),
|
"start_time and end_time."),
|
||||||
|
Param("batch", default=False,
|
||||||
|
doc="Run the sub-instance headless (-b) with its output captured "
|
||||||
|
"into this test's log/report and result value, even in the GUI."),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
def __init__(self, dict_item, parent = None, status_queue=None, filename=""):
|
||||||
@@ -90,6 +94,38 @@ class TestItemRun(TestItem):
|
|||||||
self.start_time = self._prms.getParam('start_time')
|
self.start_time = self._prms.getParam('start_time')
|
||||||
self.end_time = self._prms.getParam('end_time')
|
self.end_time = self._prms.getParam('end_time')
|
||||||
self.wait_for_exec = self._prms.getParam('wait_for_exec')
|
self.wait_for_exec = self._prms.getParam('wait_for_exec')
|
||||||
|
self.batch = self._prms.getParam('batch', default=False)
|
||||||
|
|
||||||
|
def _launch(self, cmd, capture):
|
||||||
|
"""Run the sub-instance once. When *capture*, stream its output to the
|
||||||
|
log/report, keep it as the result value, and let Stop kill the child."""
|
||||||
|
if not capture:
|
||||||
|
subprocess.run(cmd)
|
||||||
|
return
|
||||||
|
sink = []
|
||||||
|
prefix = f"[{os.path.basename(self.tum_file)}] "
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
threads = drain_to_log(proc, prefix=prefix, sink=sink)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=0.2)
|
||||||
|
break
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if self.isStopped():
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=2)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
for t in threads:
|
||||||
|
t.join(timeout=2)
|
||||||
|
# Captured log -> result value (store_result / expected_result).
|
||||||
|
self.result.value = "\n".join(sink)
|
||||||
|
|
||||||
@test_run
|
@test_run
|
||||||
def execute(self):
|
def execute(self):
|
||||||
@@ -104,25 +140,26 @@ class TestItemRun(TestItem):
|
|||||||
pf = self._prms.expanse(self.param_file)
|
pf = self._prms.expanse(self.param_file)
|
||||||
lp = self._prms.expanse(self.log_path)
|
lp = self._prms.expanse(self.log_path)
|
||||||
rp = self._prms.expanse(self.report_path)
|
rp = self._prms.expanse(self.report_path)
|
||||||
|
|
||||||
|
# Capture (headless -b) in batch or when `batch: true`; else open
|
||||||
|
# the child's own GUI window (-r).
|
||||||
|
capture = bool(self.batch) or tm.text_mode()
|
||||||
|
|
||||||
cmd = _testium_launch_cmd()
|
cmd = _testium_launch_cmd()
|
||||||
if tm.text_mode():
|
if capture:
|
||||||
cmd.append("-b")
|
cmd += ["-b", "-o"] # -o: no colour codes in the captured log
|
||||||
else:
|
else:
|
||||||
cmd.append("-r")
|
cmd.append("-r")
|
||||||
if lp == '':
|
if lp == '':
|
||||||
lp = os.path.splitext(self.tum_file)[0] + "_" + \
|
lp = os.path.splitext(self.tum_file)[0] + "_" + \
|
||||||
datetime.utcnow().isoformat(timespec='seconds') + '.log'
|
datetime.utcnow().isoformat(timespec='seconds') + '.log'
|
||||||
cmd.append("-l")
|
cmd += ["-l", '"' + lp + '"']
|
||||||
cmd.append('"' + lp + '"')
|
|
||||||
if pf != '':
|
if pf != '':
|
||||||
cmd.append("-c")
|
cmd += ["-c", '"' + pf + '"']
|
||||||
cmd.append('"' + pf + '"')
|
|
||||||
if rp != '':
|
if rp != '':
|
||||||
cmd.append("-p")
|
cmd += ["-p", '"' + rp + '"']
|
||||||
cmd.append('"' + rp + '"')
|
|
||||||
cmd.append(self.tum_file)
|
cmd.append(self.tum_file)
|
||||||
for c in cmd:
|
print(" ".join(cmd))
|
||||||
print(c, end = ' ')
|
|
||||||
|
|
||||||
if self.start_time is not None:
|
if self.start_time is not None:
|
||||||
self.start_time = datetime.strptime(
|
self.start_time = datetime.strptime(
|
||||||
@@ -135,20 +172,24 @@ class TestItemRun(TestItem):
|
|||||||
raise ETUMRuntimeError(
|
raise ETUMRuntimeError(
|
||||||
'"wait_for_exec" set but not start_time or end_time')
|
'"wait_for_exec" set but not start_time or end_time')
|
||||||
|
|
||||||
r = None
|
ran = False
|
||||||
if self.wait_for_exec:
|
if self.wait_for_exec:
|
||||||
while not nowInBetween(self.start_time, self.end_time):
|
while not nowInBetween(self.start_time, self.end_time):
|
||||||
sleep(60)
|
sleep(60)
|
||||||
r = subprocess.run(cmd)
|
self._launch(cmd, capture)
|
||||||
|
ran = True
|
||||||
elif self.start_time is not None and self.end_time is not None:
|
elif self.start_time is not None and self.end_time is not None:
|
||||||
if nowInBetween(self.start_time, self.end_time):
|
if nowInBetween(self.start_time, self.end_time):
|
||||||
r = subprocess.run(cmd)
|
self._launch(cmd, capture)
|
||||||
|
ran = True
|
||||||
elif self.start_time is not None:
|
elif self.start_time is not None:
|
||||||
if self.start_time < datetime.now().time():
|
if self.start_time < datetime.now().time():
|
||||||
r = subprocess.run(cmd)
|
self._launch(cmd, capture)
|
||||||
|
ran = True
|
||||||
else:
|
else:
|
||||||
r = subprocess.run(cmd)
|
self._launch(cmd, capture)
|
||||||
if isinstance(r, subprocess.CompletedProcess):
|
ran = True
|
||||||
|
if ran:
|
||||||
self.result.set(TestValue.SUCCESS)
|
self.result.set(TestValue.SUCCESS)
|
||||||
else:
|
else:
|
||||||
self.result.set(TestValue.FAILURE, 'Sub-test did not execute')
|
self.result.set(TestValue.FAILURE, 'Sub-test did not execute')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from time import monotonic
|
|||||||
from runtime.jrpc import RPC_PORT_SENTINEL
|
from runtime.jrpc import RPC_PORT_SENTINEL
|
||||||
|
|
||||||
|
|
||||||
def _drain_pipe(pipe, prefix):
|
def _drain_pipe(pipe, prefix, sink=None):
|
||||||
try:
|
try:
|
||||||
for raw in iter(pipe.readline, b""):
|
for raw in iter(pipe.readline, b""):
|
||||||
line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||||
@@ -23,6 +23,9 @@ def _drain_pipe(pipe, prefix):
|
|||||||
print(f"{prefix}{line}")
|
print(f"{prefix}{line}")
|
||||||
else:
|
else:
|
||||||
print(line)
|
print(line)
|
||||||
|
# sink keeps the clean (unprefixed) line for reuse as a result value
|
||||||
|
if sink is not None:
|
||||||
|
sink.append(line)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
pipe.close()
|
pipe.close()
|
||||||
@@ -30,21 +33,16 @@ def _drain_pipe(pipe, prefix):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def drain_to_log(process, prefix=""):
|
def drain_to_log(process, prefix="", sink=None):
|
||||||
"""Spawn daemon threads that read ``process.stdout`` and
|
"""Stream the subprocess stdout/stderr line by line through the parent's
|
||||||
``process.stderr`` line by line and print each line through the
|
print pipeline (log + live output). If ``sink`` is a list, each clean line
|
||||||
parent's stdout (so it reaches the log + live output).
|
is also appended to it (GIL-atomic, shared by both threads). Daemon threads."""
|
||||||
|
|
||||||
Each thread exits cleanly when the subprocess closes the
|
|
||||||
corresponding pipe (i.e. when it exits). Daemon flag ensures they
|
|
||||||
do not block testium exit.
|
|
||||||
"""
|
|
||||||
threads = []
|
threads = []
|
||||||
for pipe in (process.stdout, process.stderr):
|
for pipe in (process.stdout, process.stderr):
|
||||||
if pipe is None:
|
if pipe is None:
|
||||||
continue
|
continue
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=_drain_pipe, args=(pipe, prefix), daemon=True,
|
target=_drain_pipe, args=(pipe, prefix, sink), daemon=True,
|
||||||
)
|
)
|
||||||
t.start()
|
t.start()
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user