diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2010-02-28 13:11:21 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2010-02-28 13:11:21 -0500 |
commit | 3e012725e07941841bb4213fa9bb8b56abd01f18 (patch) | |
tree | 3fc739794ec1d1063deedf6b6c09d4f2e1889fbc | |
parent | e7ccc57e7346011bb4f5911d4268ebd3e4393cfc (diff) | |
download | python-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.py | 17 | ||||
-rw-r--r-- | coverage/execfile.py | 4 | ||||
-rw-r--r-- | test/backtest.py | 14 | ||||
-rw-r--r-- | test/coveragetest.py | 35 | ||||
-rw-r--r-- | test/test_cmdline.py | 59 | ||||
-rw-r--r-- | test/test_process.py | 23 |
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) |