summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2017-03-03 22:28:47 -0500
committerNed Batchelder <ned@nedbatchelder.com>2017-03-03 22:28:47 -0500
commit04099ca4f3585eae83de43ca7a2da96625234029 (patch)
tree41ff4c5ff47697b1085837180c9e22b1d9e66eab
parent0dfb8c8275918b8a31c75ab9e8f45828b9698036 (diff)
downloadpython-coveragepy-04099ca4f3585eae83de43ca7a2da96625234029.tar.gz
Collecting continues after saving data. #79 #448
-rw-r--r--CHANGES.rst10
-rw-r--r--coverage/collector.py28
-rw-r--r--coverage/control.py9
-rw-r--r--coverage/ctracer/tracer.c28
-rw-r--r--coverage/ctracer/tracer.h2
-rw-r--r--coverage/pytracer.py10
-rw-r--r--tests/test_api.py51
7 files changed, 121 insertions, 17 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 37938ae..02924aa 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,10 +9,20 @@ Change history for Coverage.py
Unreleased
----------
+- In previous versions, calling a method that used collected data would prevent
+ further collection. For example, `save()`, `report()`, `html_report()`, and
+ others would all stop collection. An explicit `start()` was needed to get it
+ going again. This is no longer true. Now you can use the collected data and
+ also continue measurement. Both `issue 79`_ and `issue 448`_ described this
+ problem, and have been fixed.
+
- Coverage.py has long had a special hack to support CPython's need to measure
the coverage of the standard library tests. This code was not installed by
kitted versions of coverage.py. Now it is.
+.. _issue 79: https://bitbucket.org/ned/coveragepy/issues/79/save-prevents-harvesting-on-stop
+.. _issue 448: https://bitbucket.org/ned/coveragepy/issues/448/save-and-html_report-prevent-further
+
.. _changes_434:
diff --git a/coverage/collector.py b/coverage/collector.py
index 3e28b3b..64abed4 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -162,6 +162,13 @@ class Collector(object):
"""Return the class name of the tracer we're using."""
return self._trace_class.__name__
+ def _clear_data(self):
+ """Clear out existing data, but stay ready for more collection."""
+ self.data.clear()
+
+ for tracer in self.tracers:
+ tracer.reset_activity()
+
def reset(self):
"""Clear collected data, and prepare to collect more."""
# A dictionary mapping file names to dicts with line number keys (if not
@@ -208,6 +215,8 @@ class Collector(object):
# Our active Tracers.
self.tracers = []
+ self._clear_data()
+
def _start_tracer(self):
"""Start a new Tracer object, and store it in self.tracers."""
tracer = self._trace_class()
@@ -267,6 +276,8 @@ class Collector(object):
if self._collectors:
self._collectors[-1].pause()
+ self.tracers = []
+
# Check to see whether we had a fullcoverage tracer installed. If so,
# get the stack frames it stashed away for us.
traces0 = []
@@ -309,7 +320,6 @@ class Collector(object):
)
self.pause()
- self.tracers = []
# Remove this Collector from the stack, and resume the one underneath
# (if any).
@@ -338,6 +348,14 @@ class Collector(object):
else:
self._start_tracer()
+ def activity(self):
+ """Has any activity been traced?
+
+ Returns a boolean, True if any trace function was invoked.
+
+ """
+ return any(tracer.activity() for tracer in self.tracers)
+
def switch_context(self, new_context):
"""Who-Tests-What hack: switch to a new who-context."""
# Make a new data dict, or find the existing one, and switch all the
@@ -347,11 +365,7 @@ class Collector(object):
tracer.data = data
def save_data(self, covdata):
- """Save the collected data to a `CoverageData`.
-
- Also resets the collector.
-
- """
+ """Save the collected data to a `CoverageData`."""
def abs_file_dict(d):
"""Return a dict like d, but with keys modified by `abs_file`."""
return dict((abs_file(k), v) for k, v in iitems(d))
@@ -369,4 +383,4 @@ class Collector(object):
with open(out_file, "w") as wtw_out:
pprint.pprint(self.contexts, wtw_out)
- self.reset()
+ self._clear_data()
diff --git a/coverage/control.py b/coverage/control.py
index a9b4b9e..a12eb2e 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -177,8 +177,6 @@ class Coverage(object):
self._inited = False
# Have we started collecting and not stopped it?
self._started = False
- # Have we measured some data and not harvested it?
- self._measured = False
# If we have sub-process measurement happening automatically, then we
# want any explicit creation of a Coverage object to mean, this process
@@ -671,7 +669,6 @@ class Coverage(object):
self.collector.start()
self._started = True
- self._measured = True
def stop(self):
"""Stop measuring code coverage."""
@@ -789,7 +786,7 @@ class Coverage(object):
)
def get_data(self):
- """Get the collected data and reset the collector.
+ """Get the collected data.
Also warn about various problems collecting data.
@@ -799,7 +796,8 @@ class Coverage(object):
"""
self._init()
- if not self._measured:
+
+ if not self.collector.activity():
return self.data
self.collector.save_data(self.data)
@@ -837,7 +835,6 @@ class Coverage(object):
if self.config.note:
self.data.add_run_info(note=self.config.note)
- self._measured = False
return self.data
def _find_unexecuted_files(self, src_dir):
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 619ccee..ee112d8 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -340,8 +340,8 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame)
CFileDisposition * pdisp = NULL;
-
STATS( self->stats.calls++; )
+ self->activity = TRUE;
/* Grow the stack. */
if (CTracer_set_pdata_stack(self) < 0) {
@@ -1034,7 +1034,25 @@ CTracer_stop(CTracer *self, PyObject *args_unused)
}
static PyObject *
-CTracer_get_stats(CTracer *self)
+CTracer_activity(CTracer *self, PyObject *args_unused)
+{
+ if (self->activity) {
+ Py_RETURN_TRUE;
+ }
+ else {
+ Py_RETURN_FALSE;
+ }
+}
+
+static PyObject *
+CTracer_reset_activity(CTracer *self, PyObject *args_unused)
+{
+ self->activity = FALSE;
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+CTracer_get_stats(CTracer *self, PyObject *args_unused)
{
#if COLLECT_STATS
return Py_BuildValue(
@@ -1103,6 +1121,12 @@ CTracer_methods[] = {
{ "get_stats", (PyCFunction) CTracer_get_stats, METH_VARARGS,
PyDoc_STR("Get statistics about the tracing") },
+ { "activity", (PyCFunction) CTracer_activity, METH_VARARGS,
+ PyDoc_STR("Has there been any activity?") },
+
+ { "reset_activity", (PyCFunction) CTracer_reset_activity, METH_VARARGS,
+ PyDoc_STR("Reset the activity flag") },
+
{ NULL }
};
diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h
index c174ae5..d5d630f 100644
--- a/coverage/ctracer/tracer.h
+++ b/coverage/ctracer/tracer.h
@@ -33,6 +33,8 @@ typedef struct CTracer {
BOOL started;
/* Are we tracing arcs, or just lines? */
BOOL tracing_arcs;
+ /* Have we had any activity? */
+ BOOL activity;
/*
The data stack is a stack of dictionaries. Each dictionary collects
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 452af72..3cf956f 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -52,6 +52,7 @@ class PyTracer(object):
self.last_exc_firstlineno = 0
self.thread = None
self.stopped = False
+ self._activity = False
self.in_atexit = False
# On exit, self.in_atexit = True
@@ -82,6 +83,7 @@ class PyTracer(object):
if event == 'call':
# Entering a new function context. Decide if we should trace
# in this file.
+ self._activity = True
self.data_stack.append((self.cur_file_dict, self.last_line))
filename = frame.f_code.co_filename
disp = self.should_trace_cache.get(filename)
@@ -168,6 +170,14 @@ class PyTracer(object):
sys.settrace(None)
+ def activity(self):
+ """Has there been any activity?"""
+ return self._activity
+
+ def reset_activity(self):
+ """Reset the activity() flag."""
+ self._activity = False
+
def get_stats(self):
"""Return a dictionary of statistics, or None."""
return None
diff --git a/tests/test_api.py b/tests/test_api.py
index 7530aca..07f5506 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -281,8 +281,7 @@ class ApiTest(CoverageTest):
self.start_import_stop(cov, "code2")
self.check_code1_code2(cov)
- def test_start_save_stop(self): # pragma: not covered
- self.skipTest("Expected failure: https://bitbucket.org/ned/coveragepy/issue/79")
+ def test_start_save_stop(self):
self.make_code1_code2()
cov = coverage.Coverage()
cov.start()
@@ -290,9 +289,57 @@ class ApiTest(CoverageTest):
cov.save()
import_local_file("code2")
cov.stop()
+ self.check_code1_code2(cov)
+ def test_start_save_nostop(self):
+ self.make_code1_code2()
+ cov = coverage.Coverage()
+ cov.start()
+ import_local_file("code1")
+ cov.save()
+ import_local_file("code2")
self.check_code1_code2(cov)
+ def test_two_getdata_only_warn_once(self):
+ self.make_code1_code2()
+ cov = coverage.Coverage(source=["."], omit=["code1.py"])
+ cov.start()
+ import_local_file("code1")
+ cov.stop()
+ # We didn't collect any data, so we should get a warning.
+ with self.assert_warnings(cov, ["No data was collected"]):
+ cov.get_data()
+ # But calling get_data a second time with no intervening activity
+ # won't make another warning.
+ with self.assert_warnings(cov, []):
+ cov.get_data()
+
+ def test_two_getdata_only_warn_once_nostop(self):
+ self.make_code1_code2()
+ cov = coverage.Coverage(source=["."], omit=["code1.py"])
+ cov.start()
+ import_local_file("code1")
+ # We didn't collect any data, so we should get a warning.
+ with self.assert_warnings(cov, ["No data was collected"]):
+ cov.get_data()
+ # But calling get_data a second time with no intervening activity
+ # won't make another warning.
+ with self.assert_warnings(cov, []):
+ cov.get_data()
+
+ def test_two_getdata_warn_twice(self):
+ self.make_code1_code2()
+ cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"])
+ cov.start()
+ import_local_file("code1")
+ # We didn't collect any data, so we should get a warning.
+ with self.assert_warnings(cov, ["No data was collected"]):
+ cov.save()
+ import_local_file("code2")
+ # Calling get_data a second time after tracing some more will warn again.
+ with self.assert_warnings(cov, ["No data was collected"]):
+ cov.get_data()
+
def make_good_data_files(self):
"""Make some good data files."""
self.make_code1_code2()