CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
422 lines
16 KiB
Python
422 lines
16 KiB
Python
# *************************************************************************
|
|
# * Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
|
|
# *
|
|
# * See LICENSE.txt for license information
|
|
# ************************************************************************
|
|
"""Shared fixtures for the librccl_device.bc (IR) device tests.
|
|
|
|
This mirrors test/ext-plugins: the test artifact (here, the GoogleTest
|
|
binary built from bindings/ir/test/IR_test.cpp against librccl_device.bc) is
|
|
built on demand from a session-scoped fixture and the whole suite is skipped
|
|
with a clear reason when its prerequisites are missing — instead of failing
|
|
the CI job on machines that did not build with -DEMIT_LLVM_IR=ON.
|
|
|
|
Prerequisites (set as environment variables, with sensible defaults):
|
|
|
|
RCCL_DIR RCCL source root (default: repo root, derived)
|
|
RCCL_BUILD RCCL CMake build dir (default: $RCCL_DIR/build/release)
|
|
ROCM_PATH ROCm install root (default: /opt/rocm)
|
|
ARCH offload / bitcode arch (default: gfx942)
|
|
GTEST_ROOT GoogleTest prefix (default: $RCCL_BUILD/gtest)
|
|
IR_OUTDIR dir for the compiled exe(default: <workdir>/ir_test_build)
|
|
|
|
The build requires that RCCL was configured/built with:
|
|
cmake -DEMIT_LLVM_IR=ON -DBITCODE_LIB_ARCH=<arch> -DBUILD_TESTS=ON ...
|
|
so that librccl_device.bc, the hipify-staged headers, generated nccl.h/rccl.h
|
|
and GoogleTest are all present.
|
|
"""
|
|
|
|
import glob
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
WORKDIR = os.getcwd()
|
|
|
|
# RCCL source root: this file is test/ir-device/tests/conftest.py
|
|
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
_DEFAULT_RCCL_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", "..", ".."))
|
|
|
|
RCCL_DIR = os.path.abspath(os.environ.get("RCCL_DIR", _DEFAULT_RCCL_DIR))
|
|
RCCL_BUILD = os.path.abspath(
|
|
os.environ.get("RCCL_BUILD", os.path.join(RCCL_DIR, "build", "release"))
|
|
)
|
|
ROCM_PATH = os.environ.get("ROCM_PATH", "/opt/rocm")
|
|
ARCH = os.environ.get("ARCH", "gfx942")
|
|
GTEST_ROOT = os.environ.get("GTEST_ROOT", os.path.join(RCCL_BUILD, "gtest"))
|
|
IR_OUTDIR = os.environ.get("IR_OUTDIR", os.path.join(WORKDIR, "ir_test_build"))
|
|
|
|
IR_DIR = os.path.join(RCCL_DIR, "bindings", "ir")
|
|
IR_TEST_SRC = os.path.join(IR_DIR, "test", "IR_test.cpp")
|
|
BITCODE = os.path.join(RCCL_BUILD, "lib", "librccl_device.bc")
|
|
HIPIFY_INC = os.path.join(RCCL_BUILD, "hipify", "src", "include")
|
|
GENERATED_INC = os.path.join(RCCL_BUILD, "include")
|
|
HIPCC = os.path.join(ROCM_PATH, "bin", "hipcc")
|
|
TEST_EXE = os.path.join(IR_OUTDIR, "IR_test.exe")
|
|
|
|
# --- Multi-rank GIN/composite barrier functional test (separate MPI binary) ---
|
|
# This one needs the host RCCL library + MPI in addition to the bitcode, so it
|
|
# is gated independently and skipped (not failed) when those are absent.
|
|
GIN_MPI_TEST_SRC = os.path.join(IR_DIR, "test", "IR_gin_mpi_test.cpp")
|
|
GIN_MPI_TEST_EXE = os.path.join(IR_OUTDIR, "IR_gin_mpi_test.exe")
|
|
MPI_INC = os.environ.get("MPI_INC", "/usr/include/x86_64-linux-gnu/mpich")
|
|
NRANKS = int(os.environ.get("IR_GIN_NRANKS", "2"))
|
|
# Devices the MPI run may use, one per rank. Defaults to the first NRANKS GPUs
|
|
# (0,1,...) so each rank's in-binary hipSetDevice(localRank) lands on its own
|
|
# GPU. Override with IR_GIN_GPUS="2,3" to target specific devices.
|
|
IR_GIN_GPUS = os.environ.get("IR_GIN_GPUS", ",".join(str(i) for i in range(NRANKS)))
|
|
|
|
LOGDIR = os.path.join(WORKDIR, "logs")
|
|
os.makedirs(LOGDIR, exist_ok=True)
|
|
|
|
|
|
def _find_gtest_libdir():
|
|
"""Return the dir holding libgtest.{a,so} under GTEST_ROOT, or None.
|
|
|
|
Mirrors the RCCL CMake gtest install layout (lib or lib64) and Debian
|
|
multiarch installs (lib/<triple>-linux-gnu).
|
|
"""
|
|
candidates = [
|
|
os.path.join(GTEST_ROOT, "lib"),
|
|
os.path.join(GTEST_ROOT, "lib64"),
|
|
]
|
|
candidates += glob.glob(os.path.join(GTEST_ROOT, "lib", "*-linux-gnu"))
|
|
for d in candidates:
|
|
if os.path.isfile(os.path.join(d, "libgtest.a")) or os.path.isfile(
|
|
os.path.join(d, "libgtest.so")
|
|
):
|
|
return d
|
|
return None
|
|
|
|
|
|
def _missing_prerequisite():
|
|
"""Return a human-readable reason string if a prerequisite is missing.
|
|
|
|
Note: librccl_device.bc is intentionally NOT required here — it is built
|
|
on demand from the hipify-staged headers (see _build_bitcode). The real
|
|
gate is the hipify staging dir, which a prior RCCL CMake build produces
|
|
and which is needed both to emit the bitcode and to compile the test.
|
|
"""
|
|
if not os.path.isfile(IR_TEST_SRC):
|
|
return f"IR_test.cpp not found at {IR_TEST_SRC}"
|
|
if not os.path.isfile(HIPCC):
|
|
return f"hipcc not found at {HIPCC} (set ROCM_PATH)"
|
|
if not os.path.isdir(HIPIFY_INC):
|
|
return (
|
|
f"hipify staging dir not found at {HIPIFY_INC} "
|
|
"(build RCCL once so the staged nccl_device headers exist)"
|
|
)
|
|
gtest_inc = os.path.join(GTEST_ROOT, "include")
|
|
if (
|
|
not os.path.isfile(os.path.join(gtest_inc, "gtest", "gtest.h"))
|
|
or _find_gtest_libdir() is None
|
|
):
|
|
return f"GoogleTest not found under GTEST_ROOT={GTEST_ROOT}"
|
|
if not os.path.isdir("/dev/dri") and not os.path.exists("/dev/kfd"):
|
|
return "no AMD GPU device nodes (/dev/kfd, /dev/dri) present"
|
|
return None
|
|
|
|
|
|
def _build_bitcode():
|
|
"""Emit librccl_device.bc via the standalone bindings/ir Makefile.
|
|
|
|
Equivalent to building RCCL with -DEMIT_LLVM_IR=ON, but without a full
|
|
rebuild: the Makefile reuses the hipify-staged headers an earlier RCCL
|
|
build already produced. Returns the build log path; raises on failure.
|
|
"""
|
|
env = os.environ.copy()
|
|
env.update({"ROCM_PATH": ROCM_PATH})
|
|
args = [
|
|
"make", "-C", IR_DIR,
|
|
"EMIT_LLVM_IR=1",
|
|
f"BITCODE_LIB_ARCH={ARCH}",
|
|
f"BUILDDIR={RCCL_BUILD}",
|
|
f"ROCM_PATH={ROCM_PATH}",
|
|
"llvm_ir",
|
|
]
|
|
build_log = os.path.join(LOGDIR, "ir_bitcode_build.log")
|
|
with open(build_log, "w") as log:
|
|
proc = subprocess.run(
|
|
args, env=env, stdout=log, stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
)
|
|
assert proc.returncode == 0, (
|
|
f"Failed to build librccl_device.bc (see {build_log})"
|
|
)
|
|
assert os.path.isfile(BITCODE), f"Bitcode not produced at {BITCODE}"
|
|
return build_log
|
|
|
|
|
|
def _build_test_binary():
|
|
"""Compile IR_test.cpp into a GoogleTest binary linked against the bitcode.
|
|
|
|
Ports the hipcc invocation that previously lived in run_IR_test.sh:
|
|
|
|
* -Xoffload-linker <bc> routes librccl_device.bc into the AMDGPU
|
|
device-side LTO link.
|
|
* -plugin-opt=-amdgpu-internalize-symbols=false keeps the exported
|
|
device thunks from being re-internalized by AMDGPU LTO.
|
|
* -O0 is deliberate: a ROCm 7.x AMDGPU codegen bug breaks the indirect
|
|
(vtable) dispatch this test exercises at -O1/-O2 (hang or error 700).
|
|
This is a correctness test, not a benchmark, so -O0 is fine.
|
|
|
|
Returns the build log path; raises on failure.
|
|
"""
|
|
os.makedirs(IR_OUTDIR, exist_ok=True)
|
|
gtest_inc = os.path.join(GTEST_ROOT, "include")
|
|
gtest_libdir = _find_gtest_libdir()
|
|
args = [
|
|
HIPCC,
|
|
f"--offload-arch={ARCH}", "-O0",
|
|
"-D__HIP_PLATFORM_AMD__=1",
|
|
f"-I{HIPIFY_INC}",
|
|
f"-I{os.path.join(HIPIFY_INC, 'nccl_device')}",
|
|
f"-I{GENERATED_INC}",
|
|
f"-I{gtest_inc}",
|
|
IR_TEST_SRC,
|
|
"-Xoffload-linker", BITCODE,
|
|
"-Xoffload-linker", "-plugin-opt=-amdgpu-internalize-symbols=false",
|
|
f"-L{gtest_libdir}", "-lgtest_main", "-lgtest", "-lpthread",
|
|
"-o", TEST_EXE,
|
|
]
|
|
build_log = os.path.join(LOGDIR, "ir_test_build.log")
|
|
with open(build_log, "w") as log:
|
|
log.write("$ " + " ".join(args) + "\n\n")
|
|
log.flush()
|
|
proc = subprocess.run(
|
|
args, env=os.environ.copy(), stdout=log,
|
|
stderr=subprocess.STDOUT, universal_newlines=True,
|
|
)
|
|
assert proc.returncode == 0, f"Failed to compile IR_test (see {build_log})"
|
|
assert os.path.isfile(TEST_EXE), f"Test binary not produced at {TEST_EXE}"
|
|
return build_log
|
|
|
|
|
|
def _find_rccl_libdir():
|
|
"""Return the dir holding the host librccl.so under RCCL_BUILD, or None."""
|
|
candidates = [
|
|
RCCL_BUILD,
|
|
os.path.join(RCCL_BUILD, "lib"),
|
|
os.path.join(RCCL_BUILD, "lib64"),
|
|
]
|
|
for d in candidates:
|
|
if glob.glob(os.path.join(d, "librccl.so*")):
|
|
return d
|
|
return None
|
|
|
|
|
|
def _missing_mpi_prerequisite():
|
|
"""Reason string if the MPI GIN functional test cannot be built/run.
|
|
|
|
Builds on _missing_prerequisite() (hipcc / hipify / gtest / GPU nodes) and
|
|
adds the host-library + MPI requirements unique to the multi-rank test. The
|
|
GIN-runtime gates (>=2 GPUs, IB GIN, NCCL_GIN_TYPE/CUMEM/INTRANET env) are
|
|
enforced inside the test itself via GTEST_SKIP, so they are not duplicated
|
|
here — this only covers what is needed to *build and launch* the binary.
|
|
"""
|
|
base = _missing_prerequisite()
|
|
if base:
|
|
return base
|
|
if not os.path.isfile(GIN_MPI_TEST_SRC):
|
|
return f"IR_gin_mpi_test.cpp not found at {GIN_MPI_TEST_SRC}"
|
|
if shutil.which("mpirun") is None or shutil.which("mpicxx") is None:
|
|
return "MPI not found (mpirun/mpicxx not in PATH)"
|
|
if not os.path.isfile(os.path.join(MPI_INC, "mpi.h")):
|
|
return f"mpi.h not found at {MPI_INC} (set MPI_INC)"
|
|
if _find_rccl_libdir() is None:
|
|
return (
|
|
f"host librccl.so not found under {RCCL_BUILD} "
|
|
"(build RCCL's host library, not just -DEMIT_LLVM_IR=ON)"
|
|
)
|
|
return None
|
|
|
|
|
|
def _build_gin_mpi_binary():
|
|
"""Compile IR_gin_mpi_test.cpp into an MPI GoogleTest binary.
|
|
|
|
Same device-side bitcode link as IR_test (-Xoffload-linker librccl_device.bc
|
|
+ amdgpu-internalize-symbols=false), plus the host RCCL library and MPI.
|
|
Returns the build log path; raises on failure.
|
|
"""
|
|
os.makedirs(IR_OUTDIR, exist_ok=True)
|
|
gtest_inc = os.path.join(GTEST_ROOT, "include")
|
|
gtest_libdir = _find_gtest_libdir()
|
|
rccl_libdir = _find_rccl_libdir()
|
|
args = [
|
|
HIPCC,
|
|
f"--offload-arch={ARCH}", "-O0",
|
|
"-D__HIP_PLATFORM_AMD__=1",
|
|
f"-I{MPI_INC}",
|
|
f"-I{HIPIFY_INC}",
|
|
f"-I{os.path.join(HIPIFY_INC, 'nccl_device')}",
|
|
f"-I{GENERATED_INC}",
|
|
f"-I{gtest_inc}",
|
|
GIN_MPI_TEST_SRC,
|
|
"-Xoffload-linker", BITCODE,
|
|
"-Xoffload-linker", "-plugin-opt=-amdgpu-internalize-symbols=false",
|
|
f"-L{rccl_libdir}", f"-Wl,-rpath,{rccl_libdir}", "-lrccl",
|
|
"-lmpichcxx", "-lmpich",
|
|
f"-L{gtest_libdir}", "-lgtest_main", "-lgtest", "-lpthread",
|
|
"-o", GIN_MPI_TEST_EXE,
|
|
]
|
|
build_log = os.path.join(LOGDIR, "ir_gin_mpi_build.log")
|
|
with open(build_log, "w") as log:
|
|
log.write("$ " + " ".join(args) + "\n\n")
|
|
log.flush()
|
|
proc = subprocess.run(
|
|
args, env=os.environ.copy(), stdout=log,
|
|
stderr=subprocess.STDOUT, universal_newlines=True,
|
|
)
|
|
assert proc.returncode == 0, (
|
|
f"Failed to compile IR_gin_mpi_test (see {build_log})"
|
|
)
|
|
assert os.path.isfile(GIN_MPI_TEST_EXE), (
|
|
f"MPI test binary not produced at {GIN_MPI_TEST_EXE}"
|
|
)
|
|
return build_log
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def ir_gin_mpi_binary():
|
|
"""Build the bitcode if needed, then compile the MPI GIN test once.
|
|
|
|
Skips the whole multi-rank suite when its build/launch prerequisites
|
|
(host librccl.so, MPI, hipify staging, GoogleTest, GPU nodes) are missing.
|
|
"""
|
|
reason = _missing_mpi_prerequisite()
|
|
if reason:
|
|
pytest.skip(f"IR GIN multi-rank tests skipped: {reason}")
|
|
|
|
if not os.path.isfile(BITCODE):
|
|
logger.info("librccl_device.bc MISSING — building it (arch=%s)...", ARCH)
|
|
_build_bitcode()
|
|
logger.info("Compiling IR_gin_mpi_test.cpp -> %s ...", GIN_MPI_TEST_EXE)
|
|
_build_gin_mpi_binary()
|
|
return GIN_MPI_TEST_EXE
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def run_gin_mpi_gtest(ir_gin_mpi_binary):
|
|
"""Return a helper that launches the MPI GIN test under `mpirun -np N`.
|
|
|
|
The helper returns (subprocess.CompletedProcess, log_file_path). GIN env
|
|
defaults (NCCL_GIN_TYPE=2, NCCL_CUMEM_ENABLE=1, RCCL_ENABLE_INTRANET=1) are
|
|
seeded if unset so a single-node 2-GPU run can establish GIN; the binary
|
|
itself GTEST_SKIPs cleanly if GIN still cannot initialize. HIP_VISIBLE_DEVICES
|
|
is pinned to IR_GIN_GPUS (default the first NRANKS devices) so each rank gets
|
|
its own GPU regardless of any ambient single-GPU restriction.
|
|
"""
|
|
|
|
def _run(gtest_filter, log_name):
|
|
env = os.environ.copy()
|
|
env.setdefault("NCCL_GIN_ENABLE", "1")
|
|
env.setdefault("NCCL_GIN_TYPE", "2")
|
|
env.setdefault("NCCL_CUMEM_ENABLE", "1")
|
|
env.setdefault("RCCL_ENABLE_INTRANET", "1")
|
|
env.setdefault("HSA_NO_SCRATCH_RECLAIM", "1")
|
|
# Pin GPUs per rank: expose NRANKS distinct devices to the whole run so
|
|
# each rank's hipSetDevice(localRank) selects its own. Set explicitly
|
|
# (not setdefault) so a restrictive ambient HIP_VISIBLE_DEVICES=0 from
|
|
# the shell doesn't collapse every rank onto one GPU and force a skip.
|
|
env["HIP_VISIBLE_DEVICES"] = IR_GIN_GPUS
|
|
args = [
|
|
"mpirun", "-np", str(NRANKS),
|
|
ir_gin_mpi_binary,
|
|
f"--gtest_filter={gtest_filter}", "--gtest_color=no",
|
|
]
|
|
log_file = os.path.join(LOGDIR, log_name)
|
|
with open(log_file, "w") as log:
|
|
log.write("$ " + " ".join(args) + "\n\n")
|
|
log.flush()
|
|
proc = subprocess.run(
|
|
args, env=env, stdout=log, stderr=subprocess.STDOUT,
|
|
universal_newlines=True, timeout=300,
|
|
)
|
|
return proc, log_file
|
|
|
|
return _run
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def ir_test_binary():
|
|
"""Build the bitcode if needed, then compile IR_test.cpp against it once.
|
|
|
|
Skips the entire suite if prerequisites are missing. Returns the path to
|
|
the compiled GoogleTest executable.
|
|
"""
|
|
reason = _missing_prerequisite()
|
|
if reason:
|
|
pytest.skip(f"IR device tests skipped: {reason}")
|
|
|
|
# Emit librccl_device.bc on demand (the EMIT_LLVM_IR step) when absent.
|
|
if os.path.isfile(BITCODE):
|
|
logger.info("librccl_device.bc PRESENT at %s — reusing it", BITCODE)
|
|
else:
|
|
logger.info(
|
|
"librccl_device.bc MISSING at %s — building it (EMIT_LLVM_IR step, "
|
|
"arch=%s)...", BITCODE, ARCH)
|
|
bc_log = _build_bitcode()
|
|
logger.info("librccl_device.bc BUILT at %s (log: %s)", BITCODE, bc_log)
|
|
|
|
logger.info(
|
|
"Compiling IR_test.cpp -> %s (hipcc -O0, arch=%s)...", TEST_EXE, ARCH)
|
|
test_log = _build_test_binary()
|
|
logger.info("IR_test binary BUILT at %s (log: %s)", TEST_EXE, test_log)
|
|
return TEST_EXE
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def paths():
|
|
return SimpleNamespace(
|
|
WORKDIR=WORKDIR,
|
|
RCCL_DIR=RCCL_DIR,
|
|
RCCL_BUILD=RCCL_BUILD,
|
|
ROCM_PATH=ROCM_PATH,
|
|
ARCH=ARCH,
|
|
GTEST_ROOT=GTEST_ROOT,
|
|
IR_OUTDIR=IR_OUTDIR,
|
|
BITCODE=BITCODE,
|
|
TEST_EXE=TEST_EXE,
|
|
LOGDIR=LOGDIR,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def run_gtest(ir_test_binary):
|
|
"""Return a helper that runs the IR_test binary for a --gtest_filter.
|
|
|
|
The helper returns (subprocess.CompletedProcess, log_file_path). Output is
|
|
tee'd to a log file under logs/ for post-mortem inspection.
|
|
"""
|
|
|
|
def _run(gtest_filter, log_name, gpu=None):
|
|
env = os.environ.copy()
|
|
env.setdefault("HSA_NO_SCRATCH_RECLAIM", "1")
|
|
# Default to a single GPU for determinism unless the caller overrides.
|
|
if gpu is not None:
|
|
env["HIP_VISIBLE_DEVICES"] = str(gpu)
|
|
elif "HIP_VISIBLE_DEVICES" not in env:
|
|
env["HIP_VISIBLE_DEVICES"] = "0"
|
|
|
|
args = [ir_test_binary, f"--gtest_filter={gtest_filter}",
|
|
"--gtest_color=no"]
|
|
log_file = os.path.join(LOGDIR, log_name)
|
|
with open(log_file, "w") as log:
|
|
proc = subprocess.run(
|
|
args,
|
|
env=env,
|
|
stdout=log,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
timeout=120,
|
|
)
|
|
return proc, log_file
|
|
|
|
return _run
|