summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/cmdline.py12
-rw-r--r--coverage/control.py32
-rw-r--r--coverage/misc.py4
-rw-r--r--coverage/parser.py12
-rw-r--r--coverage/report.py5
-rw-r--r--test/test_coverage.py22
6 files changed, 74 insertions, 13 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 0915b10..1029ad6 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -3,6 +3,7 @@
import optparse, sys
from coverage.execfile import run_python_file
+from coverage.misc import CoverageException
class Opts(object):
@@ -283,6 +284,8 @@ CMDS = {
}
+OK, ERR = 0, 1
+
class CoverageScript(object):
"""The command-line interface to Coverage."""
@@ -330,7 +333,6 @@ class CoverageScript(object):
"""
# Collect the command-line options.
- OK, ERR = 0, 1
if not argv:
self.help_fn(topic='minimum_help')
@@ -566,4 +568,10 @@ def main():
This is installed as the script entrypoint.
"""
- return CoverageScript().command_line(sys.argv[1:])
+ try:
+ status = CoverageScript().command_line(sys.argv[1:])
+ except CoverageException:
+ _, err, _ = sys.exc_info()
+ print(err)
+ status = ERR
+ return status
diff --git a/coverage/control.py b/coverage/control.py
index 82b9e98..0510e77 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -10,7 +10,7 @@ from coverage.collector import Collector
from coverage.data import CoverageData
from coverage.files import FileLocator
from coverage.html import HtmlReporter
-from coverage.misc import format_lines, CoverageException
+from coverage.misc import format_lines, CoverageException, NoSource
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -350,9 +350,7 @@ class Analysis(object):
if not os.path.exists(self.filename):
source = self.coverage.file_locator.get_zip_data(self.filename)
if not source:
- raise CoverageException(
- "No source for code '%s'." % self.filename
- )
+ raise NoSource("No source for code: %r" % self.filename)
self.parser = CodeParser(
text=source, filename=self.filename,
@@ -415,3 +413,29 @@ class Analysis(object):
if e not in possible and e[0] != e[1]
]
return sorted(unpredicted)
+
+ def branch_lines(self):
+ """Returns lines that have more than one exit."""
+ exit_counts = {}
+ for l1,l2 in self.arc_possibilities():
+ if l1 not in exit_counts:
+ exit_counts[l1] = 0
+ exit_counts[l1] += 1
+
+ return [l1 for l1,count in exit_counts.items() if count > 1]
+
+ def missing_branch_arcs(self):
+ """Return arcs that weren't executed from branch lines.
+
+ Returns {l1:[l2a,l2b,...], ...}
+
+ """
+ missing = self.arcs_missing()
+ branch_lines = set(self.branch_lines())
+ mba = {}
+ for l1, l2 in missing:
+ if l1 in branch_lines:
+ if l1 not in mba:
+ mba[l1] = []
+ mba[l1].append(l2)
+ return mba
diff --git a/coverage/misc.py b/coverage/misc.py
index 8a8b511..5ef982c 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -49,3 +49,7 @@ def format_lines(statements, lines):
class CoverageException(Exception):
"""An exception specific to Coverage."""
pass
+
+class NoSource(CoverageException):
+ """Used to indicate we couldn't find the source for a module."""
+ pass
diff --git a/coverage/parser.py b/coverage/parser.py
index 8957123..433cb66 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -4,7 +4,7 @@ import glob, opcode, os, re, sys, token, tokenize
from coverage.backward import set, sorted, StringIO # pylint: disable-msg=W0622
from coverage.bytecode import ByteCodes, CodeObjects
-from coverage.misc import nice_pair, CoverageException
+from coverage.misc import nice_pair, CoverageException, NoSource
class CodeParser(object):
@@ -20,9 +20,13 @@ class CodeParser(object):
assert text or filename, "CodeParser needs either text or filename"
self.filename = filename or "<code>"
if not text:
- sourcef = open(self.filename, 'rU')
- self.text = sourcef.read()
- sourcef.close()
+ try:
+ sourcef = open(self.filename, 'rU')
+ self.text = sourcef.read()
+ sourcef.close()
+ except IOError:
+ _, err, _ = sys.exc_info()
+ raise NoSource("No source for code: %r: %s" % (self.filename, err))
self.text = self.text.replace('\r\n', '\n')
self.exclude = exclude
diff --git a/coverage/report.py b/coverage/report.py
index 8b069d7..7f3e3e0 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,6 +2,7 @@
import os
from coverage.codeunit import code_unit_factory
+from coverage.misc import NoSource
class Reporter(object):
"""A base class for all reporters."""
@@ -51,8 +52,6 @@ class Reporter(object):
for cu in self.code_units:
try:
report_fn(cu, self.coverage._analyze(cu))
- except KeyboardInterrupt:
- raise
- except:
+ except NoSource:
if not self.ignore_errors:
raise
diff --git a/test/test_coverage.py b/test/test_coverage.py
index b4d5752..92aef08 100644
--- a/test/test_coverage.py
+++ b/test/test_coverage.py
@@ -1725,6 +1725,28 @@ class ProcessTest(CoverageTest):
data.read_file(".coverage")
self.assertEqual(data.summary()['b_or_c.py'], 7)
+ def test_missing_source_file(self):
+ # Check what happens if the source is missing when reporting happens.
+ self.make_file("fleeting.py", """\
+ s = 'goodbye, cruel world!'
+ """)
+
+ self.run_command("coverage run fleeting.py")
+ os.remove("fleeting.py")
+ out = self.run_command("coverage html -d htmlcov")
+ self.assert_matches(out, "No source for code: '.*fleeting.py'")
+ self.assert_("Traceback" not in out)
+
+ # It happens that the code paths are different for *.py and other files.
+ self.make_file("fleeting", """\
+ s = 'goodbye, cruel world!'
+ """)
+
+ self.run_command("coverage run fleeting")
+ os.remove("fleeting")
+ out = self.run_command("coverage html -d htmlcov")
+ self.assert_matches(out, "No source for code: '.*fleeting'")
+ self.assert_("Traceback" not in out)
class RecursionTest(CoverageTest):
"""Check what happens when recursive code gets near limits."""