diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index f12e8bbbd..1027e93df 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -122,6 +122,48 @@ __all__ = [ "ALWAYS_EQ", "LARGEST", "SMALLEST" ] +# Timeout in seconds for tests using a network server listening on the network +# local loopback interface like 127.0.0.1. +# +# The timeout is long enough to prevent test failure: it takes into account +# that the client and the server can run in different threads or even different +# processes. +# +# The timeout should be long enough for connect(), recv() and send() methods +# of socket.socket. +LOOPBACK_TIMEOUT = 5.0 +if sys.platform == 'win32' and ' 32 bit (ARM)' in sys.version: + # bpo-37553: test_socket.SendfileUsingSendTest is taking longer than 2 + # seconds on Windows ARM32 buildbot + LOOPBACK_TIMEOUT = 10 +elif sys.platform == 'vxworks': + LOOPBACK_TIMEOUT = 10 + +# Timeout in seconds for network requests going to the internet. The timeout is +# short enough to prevent a test to wait for too long if the internet request +# is blocked for whatever reason. +# +# Usually, a timeout using INTERNET_TIMEOUT should not mark a test as failed, +# but skip the test instead: see transient_internet(). +INTERNET_TIMEOUT = 60.0 + +# Timeout in seconds to mark a test as failed if the test takes "too long". +# +# The timeout value depends on the regrtest --timeout command line option. +# +# If a test using SHORT_TIMEOUT starts to fail randomly on slow buildbots, use +# LONG_TIMEOUT instead. +SHORT_TIMEOUT = 30.0 + +# Timeout in seconds to detect when a test hangs. +# +# It is long enough to reduce the risk of test failure on the slowest Python +# buildbots. It should not be used to mark a test as failed if the test takes +# "too long". The timeout value depends on the regrtest --timeout command line +# option. +LONG_TIMEOUT = 5 * 60.0 + + class Error(Exception): """Base class for regression test exceptions.""" @@ -3378,3 +3420,54 @@ class catch_threading_exception: del self.exc_value del self.exc_traceback del self.thread + + +def wait_process(pid, *, exitcode, timeout=None): + """ + Wait until process pid completes and check that the process exit code is + exitcode. + Raise an AssertionError if the process exit code is not equal to exitcode. + If the process runs longer than timeout seconds (SHORT_TIMEOUT by default), + kill the process (if signal.SIGKILL is available) and raise an + AssertionError. The timeout feature is not available on Windows. + """ + if os.name != "nt": + import signal + + if timeout is None: + timeout = SHORT_TIMEOUT + t0 = time.monotonic() + sleep = 0.001 + max_sleep = 0.1 + while True: + pid2, status = os.waitpid(pid, os.WNOHANG) + if pid2 != 0: + break + # process is still running + + dt = time.monotonic() - t0 + if dt > SHORT_TIMEOUT: + try: + os.kill(pid, signal.SIGKILL) + os.waitpid(pid, 0) + except OSError: + # Ignore errors like ChildProcessError or PermissionError + pass + + raise AssertionError(f"process {pid} is still running " + f"after {dt:.1f} seconds") + + sleep = min(sleep * 2, max_sleep) + time.sleep(sleep) + else: + # Windows implementation + pid2, status = os.waitpid(pid, 0) + + exitcode2 = os.waitstatus_to_exitcode(status) + if exitcode2 != exitcode: + raise AssertionError(f"process {pid} exited with code {exitcode2}, " + f"but exit code {exitcode} is expected") + + # sanity check: it should not fail in practice + if pid2 != pid: + raise AssertionError(f"pid {pid2} != pid {pid}") \ No newline at end of file diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 3dfce9b7d..e13fe4304 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1538,8 +1538,6 @@ class _PosixSpawnMixin: # test_close_file() to fail. return (sys.executable, '-I', '-S', *args) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_returns_pid(self): pidfile = support.TESTFN self.addCleanup(support.unlink, pidfile) @@ -1586,8 +1584,6 @@ class _PosixSpawnMixin: with open(envfile) as f: self.assertEqual(f.read(), 'bar') - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_none_file_actions(self): pid = self.spawn_func( self.NOOP_PROGRAM[0], @@ -1597,8 +1593,6 @@ class _PosixSpawnMixin: ) support.wait_process(pid, exitcode=0) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_empty_file_actions(self): pid = self.spawn_func( self.NOOP_PROGRAM[0], @@ -1795,8 +1789,6 @@ class _PosixSpawnMixin: ) support.wait_process(pid, exitcode=0) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_multiple_file_actions(self): file_actions = [ (os.POSIX_SPAWN_OPEN, 3, os.path.realpath(__file__), os.O_RDONLY, 0), @@ -1838,8 +1830,6 @@ class _PosixSpawnMixin: 3, __file__ + '\0', os.O_RDONLY, 0)]) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_open_file(self): outfile = support.TESTFN self.addCleanup(support.unlink, outfile) @@ -1860,7 +1850,7 @@ class _PosixSpawnMixin: with open(outfile) as f: self.assertEqual(f.read(), 'hello') - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' + # TODO: RUSTPYTHON: FileNotFoundError: [Errno 2] No such file or directory (os error 2): '@test_55144_tmp' -> 'None' @unittest.expectedFailure def test_close_file(self): closefile = support.TESTFN @@ -1881,8 +1871,6 @@ class _PosixSpawnMixin: with open(closefile) as f: self.assertEqual(f.read(), 'is closed %d' % errno.EBADF) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure def test_dup2(self): dupfile = support.TESTFN self.addCleanup(support.unlink, dupfile) @@ -1911,8 +1899,6 @@ class TestPosixSpawn(unittest.TestCase, _PosixSpawnMixin): class TestPosixSpawnP(unittest.TestCase, _PosixSpawnMixin): spawn_func = getattr(posix, 'posix_spawnp', None) - # TODO: RUSTPYTHON: AttributeError: module 'test.support' has no attribute 'wait_process' - @unittest.expectedFailure @support.skip_unless_symlink def test_posix_spawnp(self): # Use a symlink to create a program in its own temporary directory diff --git a/vm/src/stdlib/os.rs b/vm/src/stdlib/os.rs index 63bddbcf0..6e591d3dc 100644 --- a/vm/src/stdlib/os.rs +++ b/vm/src/stdlib/os.rs @@ -1668,6 +1668,31 @@ pub(super) mod _os { Ok((loadavg[0], loadavg[1], loadavg[2])) } + #[cfg(any(unix, windows))] + #[pyfunction] + fn waitstatus_to_exitcode(status: i32, vm: &VirtualMachine) -> PyResult { + let status = u32::try_from(status) + .map_err(|_| vm.new_value_error(format!("invalid WEXITSTATUS: {}", status)))?; + + cfg_if::cfg_if! { + if #[cfg(not(windows))] { + let status = status as libc::c_int; + if libc::WIFEXITED(status) { + return Ok(libc::WEXITSTATUS(status)); + } + + if libc::WIFSIGNALED(status) { + return Ok(-libc::WTERMSIG(status)); + } + + Err(vm.new_value_error(format!("Invalid wait status: {}", status))) + } else { + i32::try_from(status.rotate_right(8)) + .map_err(|_| vm.new_value_error(format!("invalid wait status: {}", status))) + } + } + } + #[pyattr] #[pyclass(module = "os", name = "terminal_size")] #[derive(PyStructSequence)]