Mini Shell
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Test running processes with the APIs in L{twisted.internet.utils}.
"""
import os
import signal
import stat
import sys
import warnings
from unittest import skipIf
from twisted.internet import error, interfaces, reactor, utils
from twisted.internet.defer import Deferred
from twisted.python.runtime import platform
from twisted.python.test.test_util import SuppressedWarningsTests
from twisted.trial.unittest import SynchronousTestCase, TestCase
class ProcessUtilsTests(TestCase):
"""
Test running a process using L{getProcessOutput}, L{getProcessValue}, and
L{getProcessOutputAndValue}.
"""
if interfaces.IReactorProcess(reactor, None) is None:
skip = "reactor doesn't implement IReactorProcess"
output = None
value = None
exe = sys.executable
def makeSourceFile(self, sourceLines):
"""
Write the given list of lines to a text file and return the absolute
path to it.
"""
script = self.mktemp()
with open(script, "wt") as scriptFile:
scriptFile.write(os.linesep.join(sourceLines) + os.linesep)
return os.path.abspath(script)
def test_output(self):
"""
L{getProcessOutput} returns a L{Deferred} which fires with the complete
output of the process it runs after that process exits.
"""
scriptFile = self.makeSourceFile(
[
"import sys",
"for s in b'hello world\\n':",
" s = bytes([s])",
" sys.stdout.buffer.write(s)",
" sys.stdout.flush()",
]
)
d = utils.getProcessOutput(self.exe, ["-u", scriptFile])
return d.addCallback(self.assertEqual, b"hello world\n")
def test_outputWithErrorIgnored(self):
"""
The L{Deferred} returned by L{getProcessOutput} is fired with an
L{IOError} L{Failure} if the child process writes to stderr.
"""
# make sure stderr raises an error normally
scriptFile = self.makeSourceFile(
["import sys", 'sys.stderr.write("hello world\\n")']
)
d = utils.getProcessOutput(self.exe, ["-u", scriptFile])
d = self.assertFailure(d, IOError)
def cbFailed(err):
return self.assertFailure(err.processEnded, error.ProcessDone)
d.addCallback(cbFailed)
return d
def test_outputWithErrorCollected(self):
"""
If a C{True} value is supplied for the C{errortoo} parameter to
L{getProcessOutput}, the returned L{Deferred} fires with the child's
stderr output as well as its stdout output.
"""
scriptFile = self.makeSourceFile(
[
"import sys",
# Write the same value to both because ordering isn't guaranteed so
# this simplifies the test.
'sys.stdout.write("foo")',
"sys.stdout.flush()",
'sys.stderr.write("foo")',
"sys.stderr.flush()",
]
)
d = utils.getProcessOutput(self.exe, ["-u", scriptFile], errortoo=True)
return d.addCallback(self.assertEqual, b"foofoo")
def test_value(self):
"""
The L{Deferred} returned by L{getProcessValue} is fired with the exit
status of the child process.
"""
scriptFile = self.makeSourceFile(["raise SystemExit(1)"])
d = utils.getProcessValue(self.exe, ["-u", scriptFile])
return d.addCallback(self.assertEqual, 1)
def test_outputAndValue(self):
"""
The L{Deferred} returned by L{getProcessOutputAndValue} fires with a
three-tuple, the elements of which give the data written to the child's
stdout, the data written to the child's stderr, and the exit status of
the child.
"""
scriptFile = self.makeSourceFile(
[
"import sys",
"sys.stdout.buffer.write(b'hello world!\\n')",
"sys.stderr.buffer.write(b'goodbye world!\\n')",
"sys.exit(1)",
]
)
def gotOutputAndValue(out_err_code):
out, err, code = out_err_code
self.assertEqual(out, b"hello world!\n")
self.assertEqual(err, b"goodbye world!\n")
self.assertEqual(code, 1)
d = utils.getProcessOutputAndValue(self.exe, ["-u", scriptFile])
return d.addCallback(gotOutputAndValue)
@skipIf(platform.isWindows(), "Windows doesn't have real signals.")
def test_outputSignal(self):
"""
If the child process exits because of a signal, the L{Deferred}
returned by L{getProcessOutputAndValue} fires a L{Failure} of a tuple
containing the child's stdout, stderr, and the signal which caused
it to exit.
"""
# Use SIGKILL here because it's guaranteed to be delivered. Using
# SIGHUP might not work in, e.g., a buildbot slave run under the
# 'nohup' command.
scriptFile = self.makeSourceFile(
[
"import sys, os, signal",
"sys.stdout.write('stdout bytes\\n')",
"sys.stderr.write('stderr bytes\\n')",
"sys.stdout.flush()",
"sys.stderr.flush()",
"os.kill(os.getpid(), signal.SIGKILL)",
]
)
def gotOutputAndValue(out_err_sig):
out, err, sig = out_err_sig
self.assertEqual(out, b"stdout bytes\n")
self.assertEqual(err, b"stderr bytes\n")
self.assertEqual(sig, signal.SIGKILL)
d = utils.getProcessOutputAndValue(self.exe, ["-u", scriptFile])
d = self.assertFailure(d, tuple)
return d.addCallback(gotOutputAndValue)
def _pathTest(self, utilFunc, check):
dir = os.path.abspath(self.mktemp())
os.makedirs(dir)
scriptFile = self.makeSourceFile(
["import os, sys", "sys.stdout.write(os.getcwd())"]
)
d = utilFunc(self.exe, ["-u", scriptFile], path=dir)
d.addCallback(check, dir.encode(sys.getfilesystemencoding()))
return d
def test_getProcessOutputPath(self):
"""
L{getProcessOutput} runs the given command with the working directory
given by the C{path} parameter.
"""
return self._pathTest(utils.getProcessOutput, self.assertEqual)
def test_getProcessValuePath(self):
"""
L{getProcessValue} runs the given command with the working directory
given by the C{path} parameter.
"""
def check(result, ignored):
self.assertEqual(result, 0)
return self._pathTest(utils.getProcessValue, check)
def test_getProcessOutputAndValuePath(self):
"""
L{getProcessOutputAndValue} runs the given command with the working
directory given by the C{path} parameter.
"""
def check(out_err_status, dir):
out, err, status = out_err_status
self.assertEqual(out, dir)
self.assertEqual(status, 0)
return self._pathTest(utils.getProcessOutputAndValue, check)
def _defaultPathTest(self, utilFunc, check):
# Make another directory to mess around with.
dir = os.path.abspath(self.mktemp())
os.makedirs(dir)
scriptFile = self.makeSourceFile(
["import os, sys", "cdir = os.getcwd()", "sys.stdout.write(cdir)"]
)
# Switch to it, but make sure we switch back
self.addCleanup(os.chdir, os.getcwd())
os.chdir(dir)
# Remember its default permissions.
originalMode = stat.S_IMODE(os.stat(".").st_mode)
# On macOS Catalina (and maybe elsewhere), os.getcwd() sometimes fails
# with EACCES if u+rx is missing from the working directory, so don't
# reduce it further than this.
os.chmod(dir, stat.S_IXUSR | stat.S_IRUSR)
# Restore the permissions to their original state later (probably
# adding at least u+w), because otherwise it might be hard to delete
# the trial temporary directory.
self.addCleanup(os.chmod, dir, originalMode)
d = utilFunc(self.exe, ["-u", scriptFile])
d.addCallback(check, dir.encode(sys.getfilesystemencoding()))
return d
def test_getProcessOutputDefaultPath(self):
"""
If no value is supplied for the C{path} parameter, L{getProcessOutput}
runs the given command in the same working directory as the parent
process and succeeds even if the current working directory is not
accessible.
"""
return self._defaultPathTest(utils.getProcessOutput, self.assertEqual)
def test_getProcessValueDefaultPath(self):
"""
If no value is supplied for the C{path} parameter, L{getProcessValue}
runs the given command in the same working directory as the parent
process and succeeds even if the current working directory is not
accessible.
"""
def check(result, ignored):
self.assertEqual(result, 0)
return self._defaultPathTest(utils.getProcessValue, check)
def test_getProcessOutputAndValueDefaultPath(self):
"""
If no value is supplied for the C{path} parameter,
L{getProcessOutputAndValue} runs the given command in the same working
directory as the parent process and succeeds even if the current
working directory is not accessible.
"""
def check(out_err_status, dir):
out, err, status = out_err_status
self.assertEqual(out, dir)
self.assertEqual(status, 0)
return self._defaultPathTest(utils.getProcessOutputAndValue, check)
def test_get_processOutputAndValueStdin(self):
"""
Standard input can be made available to the child process by passing
bytes for the `stdinBytes` parameter.
"""
scriptFile = self.makeSourceFile(
[
"import sys",
"sys.stdout.write(sys.stdin.read())",
]
)
stdinBytes = b"These are the bytes to see."
d = utils.getProcessOutputAndValue(
self.exe,
["-u", scriptFile],
stdinBytes=stdinBytes,
)
def gotOutputAndValue(out_err_code):
out, err, code = out_err_code
# Avoid making an exact equality comparison in case there is extra
# random output on stdout (warnings, stray print statements,
# logging, who knows).
self.assertIn(stdinBytes, out)
self.assertEqual(0, code)
d.addCallback(gotOutputAndValue)
return d
class SuppressWarningsTests(SynchronousTestCase):
"""
Tests for L{utils.suppressWarnings}.
"""
def test_suppressWarnings(self):
"""
L{utils.suppressWarnings} decorates a function so that the given
warnings are suppressed.
"""
result = []
def showwarning(self, *a, **kw):
result.append((a, kw))
self.patch(warnings, "showwarning", showwarning)
def f(msg):
warnings.warn(msg)
g = utils.suppressWarnings(f, (("ignore",), dict(message="This is message")))
# Start off with a sanity check - calling the original function
# should emit the warning.
f("Sanity check message")
self.assertEqual(len(result), 1)
# Now that that's out of the way, call the wrapped function, and
# make sure no new warnings show up.
g("This is message")
self.assertEqual(len(result), 1)
# Finally, emit another warning which should not be ignored, and
# make sure it is not.
g("Unignored message")
self.assertEqual(len(result), 2)
class DeferredSuppressedWarningsTests(SuppressedWarningsTests):
"""
Tests for L{utils.runWithWarningsSuppressed}, the version that supports
Deferreds.
"""
# Override the non-Deferred-supporting function from the base class with
# the function we are testing in this class:
runWithWarningsSuppressed = staticmethod(utils.runWithWarningsSuppressed)
def test_deferredCallback(self):
"""
If the function called by L{utils.runWithWarningsSuppressed} returns a
C{Deferred}, the warning filters aren't removed until the Deferred
fires.
"""
filters = [(("ignore", ".*foo.*"), {}), (("ignore", ".*bar.*"), {})]
result = Deferred()
self.runWithWarningsSuppressed(filters, lambda: result)
warnings.warn("ignore foo")
result.callback(3)
warnings.warn("ignore foo 2")
self.assertEqual(["ignore foo 2"], [w["message"] for w in self.flushWarnings()])
def test_deferredErrback(self):
"""
If the function called by L{utils.runWithWarningsSuppressed} returns a
C{Deferred}, the warning filters aren't removed until the Deferred
fires with an errback.
"""
filters = [(("ignore", ".*foo.*"), {}), (("ignore", ".*bar.*"), {})]
result = Deferred()
d = self.runWithWarningsSuppressed(filters, lambda: result)
warnings.warn("ignore foo")
result.errback(ZeroDivisionError())
d.addErrback(lambda f: f.trap(ZeroDivisionError))
warnings.warn("ignore foo 2")
self.assertEqual(["ignore foo 2"], [w["message"] for w in self.flushWarnings()])
Zerion Mini Shell 1.0