diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2017-03-03 22:28:47 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2017-03-03 22:28:47 -0500 |
commit | 04099ca4f3585eae83de43ca7a2da96625234029 (patch) | |
tree | 41ff4c5ff47697b1085837180c9e22b1d9e66eab | |
parent | 0dfb8c8275918b8a31c75ab9e8f45828b9698036 (diff) | |
download | python-coveragepy-04099ca4f3585eae83de43ca7a2da96625234029.tar.gz |
Collecting continues after saving data. #79 #448
-rw-r--r-- | CHANGES.rst | 10 | ||||
-rw-r--r-- | coverage/collector.py | 28 | ||||
-rw-r--r-- | coverage/control.py | 9 | ||||
-rw-r--r-- | coverage/ctracer/tracer.c | 28 | ||||
-rw-r--r-- | coverage/ctracer/tracer.h | 2 | ||||
-rw-r--r-- | coverage/pytracer.py | 10 | ||||
-rw-r--r-- | tests/test_api.py | 51 |
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() |