summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2010-02-28 13:11:21 -0500
committerNed Batchelder <ned@nedbatchelder.com>2010-02-28 13:11:21 -0500
commit3e012725e07941841bb4213fa9bb8b56abd01f18 (patch)
tree3fc739794ec1d1063deedf6b6c09d4f2e1889fbc
parente7ccc57e7346011bb4f5911d4268ebd3e4393cfc (diff)
downloadpython-coveragepy-git-3e012725e07941841bb4213fa9bb8b56abd01f18.tar.gz
If the user's code calls sys.exit(), honor the request and exit with that status. Fixes issue #50.
-rw-r--r--coverage/cmdline.py17
-rw-r--r--coverage/execfile.py4
-rw-r--r--test/backtest.py14
-rw-r--r--test/coveragetest.py35
-rw-r--r--test/test_cmdline.py59
-rw-r--r--test/test_process.py23
6 files changed, 130 insertions, 22 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index c9383689..9e15074b 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -589,25 +589,30 @@ Coverage.py, version %(__version__)s. %(__url__)s
"""
-def main():
+def main(argv=None):
"""The main entrypoint to Coverage.
This is installed as the script entrypoint.
"""
+ if argv is None:
+ argv = sys.argv[1:]
try:
- status = CoverageScript().command_line(sys.argv[1:])
+ status = CoverageScript().command_line(argv)
except ExceptionDuringRun:
# An exception was caught while running the product code. The
# sys.exc_info() return tuple is packed into an ExceptionDuringRun
- # exception. Note that the Python interpreter doesn't print SystemExit
- # tracebacks, so it's important that we don't also.
+ # exception.
_, err, _ = sys.exc_info()
- if not isinstance(err.args[1], SystemExit):
- traceback.print_exception(*err.args)
+ traceback.print_exception(*err.args)
status = ERR
except CoverageException:
+ # A controlled error inside coverage.py: print the message to the user.
_, err, _ = sys.exc_info()
print(err)
status = ERR
+ except SystemExit:
+ # The user called `sys.exit()`. Exit with their status code.
+ _, err, _ = sys.exc_info()
+ status = err.args[0]
return status
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 1a2ffadd..333163f8 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -51,6 +51,10 @@ def run_python_file(filename, args):
# Execute the source file.
try:
exec_code_object(code, main_mod.__dict__)
+ except SystemExit:
+ # The user called sys.exit(). Just pass it along to the upper
+ # layers, where it will be handled.
+ raise
except:
# Something went wrong while executing the user code.
# Get the exc_info, and pack them into an exception that we can
diff --git a/test/backtest.py b/test/backtest.py
index 4460a78d..05a1e142 100644
--- a/test/backtest.py
+++ b/test/backtest.py
@@ -10,19 +10,19 @@ import os
try:
import subprocess
except ImportError:
- def run_command(cmd):
+ def run_command(cmd, status=0):
"""Run a command in a subprocess.
- Returns the exit code and the combined stdout and stderr.
+ Returns the exit status code and the combined stdout and stderr.
"""
_, stdouterr = os.popen4(cmd)
- return 0, stdouterr.read()
+ return status, stdouterr.read()
else:
- def run_command(cmd):
+ def run_command(cmd, status=0):
"""Run a command in a subprocess.
- Returns the exit code and the combined stdout and stderr.
+ Returns the exit status code and the combined stdout and stderr.
"""
@@ -30,7 +30,7 @@ else:
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
- retcode = proc.wait()
+ status = proc.wait()
# Get the output, and canonicalize it to strings with newlines.
output = proc.stdout.read()
@@ -38,7 +38,7 @@ else:
output = output.decode('utf-8')
output = output.replace('\r', '')
- return retcode, output
+ return status, output
# No more execfile in Py3k
try:
diff --git a/test/coveragetest.py b/test/coveragetest.py
index 853db943..54b4bd43 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -49,10 +49,16 @@ class CoverageTest(TestCase):
# Record environment variables that we changed with set_environ.
self.environ_undos = {}
- # Use a Tee to capture stdout.
+ # Capture stdout and stderr so we can examine them in tests.
+ # nose keeps stdout from littering the screen, so we can safely Tee it,
+ # but it doesn't capture stderr, so we don't want to Tee stderr to the
+ # real stderr, since it will interfere with our nice field of dots.
self.old_stdout = sys.stdout
self.captured_stdout = StringIO()
sys.stdout = Tee(sys.stdout, self.captured_stdout)
+ self.old_stderr = sys.stderr
+ self.captured_stderr = StringIO()
+ sys.stderr = self.captured_stderr
def tearDown(self):
if self.run_in_temp_dir:
@@ -66,8 +72,9 @@ class CoverageTest(TestCase):
# Restore the environment.
self.undo_environ()
- # Restore stdout.
+ # Restore stdout and stderr
sys.stdout = self.old_stdout
+ sys.stderr = self.old_stderr
def set_environ(self, name, value):
"""Set an environment variable `name` to be `value`.
@@ -99,6 +106,10 @@ class CoverageTest(TestCase):
"""Return the data written to stdout during the test."""
return self.captured_stdout.getvalue()
+ def stderr(self):
+ """Return the data written to stderr during the test."""
+ return self.captured_stderr.getvalue()
+
def make_file(self, filename, text):
"""Create a temp file.
@@ -292,6 +303,22 @@ class CoverageTest(TestCase):
Returns the process' stdout text.
"""
+ _, output = self.run_command_status(cmd)
+ return output
+
+ def run_command_status(self, cmd, status=0):
+ """Run the command-line `cmd` in a subprocess, and print its output.
+
+ Use this when you need to test the process behavior of coverage.
+
+ Compare with `command_line`.
+
+ Returns a pair: the process' exit status and stdout text.
+
+ The `status` argument is returned as the status on older Pythons where
+ we can't get the actual exit status of the process.
+
+ """
# Add our test modules directory to PYTHONPATH. I'm sure there's too
# much path munging here, but...
here = os.path.dirname(self.nice_file(coverage.__file__, ".."))
@@ -303,6 +330,6 @@ class CoverageTest(TestCase):
pypath += testmods + os.pathsep + zipfile
self.set_environ('PYTHONPATH', pypath)
- _, output = run_command(cmd)
+ status, output = run_command(cmd, status=status)
print(output)
- return output
+ return status, output
diff --git a/test/test_cmdline.py b/test/test_cmdline.py
index 56242681..c530f890 100644
--- a/test/test_cmdline.py
+++ b/test/test_cmdline.py
@@ -3,6 +3,8 @@
import os, pprint, re, shlex, sys, textwrap, unittest
import mock
import coverage
+import coverage.cmdline
+from coverage.misc import ExceptionDuringRun
sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
from coveragetest import CoverageTest, OK, ERR
@@ -537,5 +539,62 @@ class CmdLineStdoutTest(CmdLineTest):
assert "help" in out
+class CmdMainTest(CoverageTest):
+ """Tests of coverage.cmdline.main(), using mocking for isolation."""
+
+ class CoverageScriptStub(object):
+ """A stub for coverage.cmdline.CoverageScript, used by CmdMainTest."""
+
+ def command_line(self, argv):
+ """Stub for command_line, the arg determines what it will do."""
+ if argv[0] == 'hello':
+ print("Hello, world!")
+ elif argv[0] == 'raise':
+ try:
+ raise Exception("oh noes!")
+ except:
+ raise ExceptionDuringRun(*sys.exc_info())
+ elif argv[0] == 'internalraise':
+ raise ValueError("coverage is broken")
+ elif argv[0] == 'exit':
+ sys.exit(23)
+ else:
+ raise AssertionError("Bad CoverageScriptStub: %r"% (argv,))
+ return 0
+
+ def setUp(self):
+ super(CmdMainTest, self).setUp()
+ self.old_CoverageScript = coverage.cmdline.CoverageScript
+ coverage.cmdline.CoverageScript = self.CoverageScriptStub
+
+ def tearDown(self):
+ coverage.cmdline.CoverageScript = self.old_CoverageScript
+ super(CmdMainTest, self).tearDown()
+
+ def test_normal(self):
+ ret = coverage.cmdline.main(['hello'])
+ self.assertEqual(ret, 0)
+ self.assertEqual(self.stdout(), "Hello, world!\n")
+
+ def test_raise(self):
+ ret = coverage.cmdline.main(['raise'])
+ self.assertEqual(ret, 1)
+ self.assertEqual(self.stdout(), "")
+ err = self.stderr().split('\n')
+ self.assertEqual(err[0], 'Traceback (most recent call last):')
+ self.assertEqual(err[-3], ' raise Exception("oh noes!")')
+ self.assertEqual(err[-2], 'Exception: oh noes!')
+
+ def test_internalraise(self):
+ self.assertRaisesRegexp(ValueError,
+ "coverage is broken",
+ coverage.cmdline.main, ['internalraise']
+ )
+
+ def test_exit(self):
+ ret = coverage.cmdline.main(['exit'])
+ self.assertEqual(ret, 23)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/test/test_process.py b/test/test_process.py
index aadf275d..fca79d48 100644
--- a/test/test_process.py
+++ b/test/test_process.py
@@ -120,6 +120,13 @@ class ProcessTest(CoverageTest):
data.read_file(".coverage")
self.assertEqual(data.summary()['b_or_c.py'], 7)
+ # TODO
+ ## Reporting should still work even with the .rc file
+ #out = self.run_command("coverage report")
+ #self.assertMultiLineEqual(out, """\
+ # hello
+ # """)
+
def test_missing_source_file(self):
# Check what happens if the source is missing when reporting happens.
self.make_file("fleeting.py", """\
@@ -140,14 +147,16 @@ class ProcessTest(CoverageTest):
self.run_command("coverage run fleeting")
os.remove("fleeting")
- out = self.run_command("coverage html -d htmlcov")
+ status, out = self.run_command_status("coverage html -d htmlcov", 1)
self.assertRegexpMatches(out, "No source for code: '.*fleeting'")
self.assertFalse("Traceback" in out)
+ self.assertEqual(status, 1)
def test_running_missing_file(self):
- out = self.run_command("coverage run xyzzy.py")
+ status, out = self.run_command_status("coverage run xyzzy.py", 1)
self.assertRegexpMatches(out, "No file to run: .*xyzzy.py")
self.assertFalse("Traceback" in out)
+ self.assertEqual(status, 1)
def test_code_throws(self):
self.make_file("throw.py", """\
@@ -162,7 +171,7 @@ class ProcessTest(CoverageTest):
# The important thing is for "coverage run" and "python" to report the
# same traceback.
- out = self.run_command("coverage run throw.py")
+ status, out = self.run_command_status("coverage run throw.py", 1)
out2 = self.run_command("python throw.py")
self.assertMultiLineEqual(out, out2)
@@ -170,6 +179,7 @@ class ProcessTest(CoverageTest):
self.assertTrue('File "throw.py", line 5, in f2' in out)
self.assertTrue('raise Exception("hey!")' in out)
self.assertFalse('coverage' in out)
+ self.assertEqual(status, 1)
def test_code_exits(self):
self.make_file("exit.py", """\
@@ -186,6 +196,9 @@ class ProcessTest(CoverageTest):
# The important thing is for "coverage run" and "python" to have the
# same output. No traceback.
- out = self.run_command("coverage run exit.py")
- out2 = self.run_command("python exit.py")
+ status, out = self.run_command_status("coverage run exit.py", 17)
+ status2, out2 = self.run_command_status("python exit.py", 17)
self.assertMultiLineEqual(out, out2)
+ self.assertMultiLineEqual(out, "about to exit..\n")
+ self.assertEqual(status, status2)
+ self.assertEqual(status, 17)