From 880f824103357172fba805a4206dccf9e0da1f14 Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@lothar.com>
Date: Thu, 28 Jan 2010 09:39:04 -0800
Subject: [PATCH] code coverage: replace figleaf with coverage.py, should work
 on py2.6 now.

It still lacks the right HTML report (the builtin report is very pretty, but
lacks the "lines uncovered" numbers that I want), and the half-finished
delta-from-last-run measurements.
---
 .darcs-boringfile                    |   4 +-
 Makefile                             |  70 ++++++++------
 misc/{figleaf.el => coverage.el}     |  72 +++++---------
 misc/coverage2el.py                  |  45 +++++++++
 misc/coverage2text.py                | 116 ++++++++++++++++++++++
 src/allmydata/test/trial_coverage.py | 110 +++++++++++++++++++++
 src/allmydata/test/trial_figleaf.py  | 139 ---------------------------
 twisted/plugins/allmydata_trial.py   |  41 ++++----
 8 files changed, 357 insertions(+), 240 deletions(-)
 rename misc/{figleaf.el => coverage.el} (65%)
 create mode 100755 misc/coverage2el.py
 create mode 100755 misc/coverage2text.py
 create mode 100644 src/allmydata/test/trial_coverage.py
 delete mode 100644 src/allmydata/test/trial_figleaf.py

diff --git a/.darcs-boringfile b/.darcs-boringfile
index 09913bd0..908d6c08 100644
--- a/.darcs-boringfile
+++ b/.darcs-boringfile
@@ -52,10 +52,10 @@
 ^build($|/)
 ^build-stamp$
 ^python-build-stamp-2.[45]$
-^\.figleaf$
+^\.coverage$
 ^coverage-html($|/)
 ^twisted/plugins/dropin\.cache$
-^\.figleaf\.el$
+^\.coverage\.el$
 ^_test_memory($|/)
 
 # _version.py is generated at build time, and never checked in
diff --git a/Makefile b/Makefile
index e220a518..0b29820c 100644
--- a/Makefile
+++ b/Makefile
@@ -81,8 +81,8 @@ endif
 
 # TESTING
 
-.PHONY: signal-error-deps test test-figleaf quicktest quicktest-figleaf
-.PHONY: figleaf-output get-old-figleaf-coverage figleaf-delta-output
+.PHONY: signal-error-deps test test-coverage quicktest quicktest-coverage
+.PHONY: coverage-output get-old-coverage-coverage coverage-delta-output
 
 
 signal-error-deps:
@@ -114,41 +114,55 @@ test: build src/allmydata/_version.py
 fuse-test: .built .checked-deps
 	$(RUNPP) -d contrib/fuse -p -c runtests.py
 
-test-figleaf: build src/allmydata/_version.py
-	rm -f .figleaf
-	$(PYTHON) setup.py trial --reporter=bwverbose-figleaf -s $(TEST)
+test-coverage: build src/allmydata/_version.py
+	rm -f .coverage
+	$(PYTHON) setup.py trial --reporter=bwverbose-coverage -s $(TEST)
 
 quicktest:
 	$(PYTHON) misc/run-with-pythonpath.py trial $(TRIALARGS) $(TEST)
 
-quicktest-figleaf:
-	rm -f .figleaf
-	$(PYTHON) misc/run-with-pythonpath.py trial --reporter=bwverbose-figleaf $(TEST)
+# code-coverage: install the "coverage" package from PyPI, do "make
+# quicktest-coverage" to do a unit test run with coverage-gathering enabled,
+# then use "make coverate-output-text" for a brief report, or "make
+# coverage-output" for a pretty HTML report. Also see "make .coverage.el" and
+# misc/coverage.el for emacs integration.
 
-figleaf-output:
-	$(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes"
-	cp .figleaf coverage-html/figleaf.pickle
-	@echo "now point your browser at coverage-html/index.html"
+quicktest-coverage:
+	rm -f .coverage
+	$(PYTHON) misc/run-with-pythonpath.py trial --reporter=bwverbose-coverage $(TEST)
+# on my laptop, "quicktest" takes 239s, "quicktest-coverage" takes 304s
+
+COVERAGE_OMIT = --omit /System,/Library,/usr/lib,src/allmydata/test,support
 
-# use these two targets to compare this coverage against the previous run.
-# The deltas only work if the old test was run in the same directory, since
-# it compares absolute filenames.
-get-old-figleaf-coverage:
-	wget --progress=dot -O old.figleaf http://allmydata.org/tahoe-figleaf/current/figleaf.pickle
+# this is like 'coverage report', but includes lines-uncovered
+coverage-output-text:
+	$(PYTHON) misc/coverage2text.py
 
-figleaf-delta-output:
-	$(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes -o old.figleaf"
-	cp .figleaf coverage-html/figleaf.pickle
+coverage-output:
+	rm -rf coverage-html
+	coverage html -d coverage-html $(COVERAGE_OMIT)
+	cp .coverage coverage-html/coverage.data
 	@echo "now point your browser at coverage-html/index.html"
 
-# after doing test-figleaf and figleaf-output, point your browser at
-# coverage-html/index.html
+## use these two targets to compare this coverage against the previous run.
+## The deltas only work if the old test was run in the same directory, since
+## it compares absolute filenames.
+#get-old-figleaf-coverage:
+#	wget --progress=dot -O old.figleaf http://allmydata.org/tahoe-figleaf/current/figleaf.pickle
+#
+#figleaf-delta-output:
+#	$(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes -o old.figleaf"
+#	cp .figleaf coverage-html/figleaf.pickle
+#	@echo "now point your browser at coverage-html/index.html"
 
-.PHONY: upload-figleaf .figleaf.el pyflakes count-lines
+.PHONY: upload-coverage .coverage.el pyflakes count-lines
 .PHONY: check-memory check-memory-once check-speed check-grid
 .PHONY: repl test-darcs-boringfile test-clean clean find-trailing-spaces
 
-# 'upload-figleaf' is meant to be run with an UPLOAD_TARGET=host:/dir setting
+.coverage.el: .coverage
+	$(PYTHON) misc/coverage2el.py
+
+# 'upload-coverage' is meant to be run with an UPLOAD_TARGET=host:/dir setting
 ifdef UPLOAD_TARGET
 
 ifndef UPLOAD_HOST
@@ -158,17 +172,15 @@ ifndef COVERAGEDIR
 $(error COVERAGEDIR must be set when using UPLOAD_TARGET)
 endif
 
-upload-figleaf:
+upload-coverage:
 	rsync -a coverage-html/ $(UPLOAD_TARGET)
-	ssh $(UPLOAD_HOST) make update-tahoe-figleaf COVERAGEDIR=$(COVERAGEDIR)
+	ssh $(UPLOAD_HOST) make update-tahoe-coverage COVERAGEDIR=$(COVERAGEDIR)
 else
-upload-figleaf:
+upload-coverage:
 	echo "this target is meant to be run with UPLOAD_TARGET=host:/path/"
 	false
 endif
 
-.figleaf.el: .figleaf
-	$(RUNPP) -p -c "misc/figleaf2el.py .figleaf src"
 
 pyflakes:
 	$(PYTHON) -OOu `which pyflakes` src/allmydata |sort |uniq
diff --git a/misc/figleaf.el b/misc/coverage.el
similarity index 65%
rename from misc/figleaf.el
rename to misc/coverage.el
index fef42e08..64e7134e 100644
--- a/misc/figleaf.el
+++ b/misc/coverage.el
@@ -1,52 +1,33 @@
 
-;(require 'gnus-start)
+(defvar coverage-annotation-file ".coverage.el")
+(defvar coverage-annotations nil)
 
-; (defun gnus-load (file)
-;   "Load FILE, but in such a way that read errors can be reported."
-;   (with-temp-buffer
-;     (insert-file-contents file)
-;     (while (not (eobp))
-;       (condition-case type
-; 	  (let ((form (read (current-buffer))))
-; 	    (eval form))
-; 	(error
-; 	 (unless (eq (car type) 'end-of-file)
-; 	   (let ((error (format "Error in %s line %d" file
-; 				(count-lines (point-min) (point)))))
-; 	     (ding)
-; 	     (unless (gnus-yes-or-no-p (concat error "; continue? "))
-; 	       (error "%s" error)))))))))
-
-(defvar figleaf-annotation-file ".figleaf.el")
-(defvar figleaf-annotations nil)
-
-(defun find-figleaf-annotation-file ()
+(defun find-coverage-annotation-file ()
   (let ((dir (file-name-directory buffer-file-name))
         (olddir "/"))
     (while (and (not (equal dir olddir))
-                (not (file-regular-p (concat dir figleaf-annotation-file))))
+                (not (file-regular-p (concat dir coverage-annotation-file))))
       (setq olddir dir
             dir (file-name-directory (directory-file-name dir))))
-    (and (not (equal dir olddir)) (concat dir figleaf-annotation-file))
+    (and (not (equal dir olddir)) (concat dir coverage-annotation-file))
 ))
 
-(defun load-figleaf-annotations ()
-  (let* ((annotation-file (find-figleaf-annotation-file))
+(defun load-coverage-annotations ()
+  (let* ((annotation-file (find-coverage-annotation-file))
          (coverage
           (with-temp-buffer
             (insert-file-contents annotation-file)
             (let ((form (read (current-buffer))))
               (eval form)))))
-    (setq figleaf-annotations coverage)
+    (setq coverage-annotations coverage)
     coverage
     ))
 
-(defun figleaf-unannotate ()
-  (interactive)
+(defun coverage-unannotate ()
   (save-excursion
     (dolist (ov (overlays-in (point-min) (point-max)))
       (delete-overlay ov))
-    (setq figleaf-this-buffer-is-annotated nil)
+    (setq coverage-this-buffer-is-annotated nil)
     (message "Removed annotations")
 ))
 
@@ -62,10 +43,9 @@
 ;; overriding actual program text), and to modify the text being displayed
 ;; (by changing its background color, or adding a box around each word).
 
-(defun figleaf-annotate (&optional show-code)
-  (interactive "P")
-  (let ((allcoverage (load-figleaf-annotations))
-        (filename-key buffer-file-name)
+(defun coverage-annotate (show-code)
+  (let ((allcoverage (load-coverage-annotations))
+        (filename-key buffer-file-truename)
         thiscoverage code-lines covered-lines uncovered-code-lines
         )
     (while (and (not (gethash filename-key allcoverage nil))
@@ -76,7 +56,7 @@
     (setq thiscoverage (gethash filename-key allcoverage nil))
     (if thiscoverage
         (progn
-          (setq figleaf-this-buffer-is-annotated t)
+          (setq coverage-this-buffer-is-annotated t)
           (setq code-lines (nth 0 thiscoverage)
                 covered-lines (nth 1 thiscoverage)
                 uncovered-code-lines (nth 2 thiscoverage)
@@ -110,31 +90,31 @@
       (message "unable to find coverage for this file"))
 ))
 
-(defun figleaf-toggle-annotations (show-code)
+(defun coverage-toggle-annotations (show-code)
   (interactive "P")
-  (if figleaf-this-buffer-is-annotated
-      (figleaf-unannotate)
-    (figleaf-annotate show-code))
+  (if coverage-this-buffer-is-annotated
+      (coverage-unannotate)
+    (coverage-annotate show-code))
 )
 
 
-(setq figleaf-this-buffer-is-annotated nil)
-(make-variable-buffer-local 'figleaf-this-buffer-is-annotated)
+(setq coverage-this-buffer-is-annotated nil)
+(make-variable-buffer-local 'coverage-this-buffer-is-annotated)
 
-(define-minor-mode figleaf-annotation-minor-mode
+(define-minor-mode coverage-annotation-minor-mode
   "Minor mode to annotate code-coverage information"
   nil
-  " FA"
+  " CA"
   '(
-    ("\C-c\C-a" . figleaf-toggle-annotations)
+    ("\C-c\C-a" . coverage-toggle-annotations)
     )
 
   () ; forms run on mode entry/exit
 )
 
-(defun maybe-enable-figleaf-mode ()
+(defun maybe-enable-coverage-mode ()
   (if (string-match "/src/allmydata/" (buffer-file-name))
-      (figleaf-annotation-minor-mode t)
+      (coverage-annotation-minor-mode t)
     ))
 
-(add-hook 'python-mode-hook 'maybe-enable-figleaf-mode)
+(add-hook 'python-mode-hook 'maybe-enable-coverage-mode)
diff --git a/misc/coverage2el.py b/misc/coverage2el.py
new file mode 100755
index 00000000..ed94bd0f
--- /dev/null
+++ b/misc/coverage2el.py
@@ -0,0 +1,45 @@
+
+from coverage import coverage, summary
+
+class ElispReporter(summary.SummaryReporter):
+    def report(self):
+        self.find_code_units(None, ["/System", "/Library", "/usr/lib",
+                                    "support/lib", "src/allmydata/test"])
+
+        out = open(".coverage.el", "w")
+        out.write("""
+;; This is an elisp-readable form of the figleaf coverage data. It defines a
+;; single top-level hash table in which the key is an asolute pathname, and
+;; the value is a three-element list. The first element of this list is a
+;; list of line numbers that represent actual code statements. The second is
+;; a list of line numbers for lines which got used during the unit test. The
+;; third is a list of line numbers for code lines that were not covered
+;; (since 'code' and 'covered' start as sets, this last list is equal to
+;; 'code - covered').
+
+    """)
+        out.write("(let ((results (make-hash-table :test 'equal)))\n")
+        for cu in self.code_units:
+            f = cu.filename
+            (fn, executable, missing, mf) = self.coverage.analysis(cu)
+            code_linenumbers = executable
+            uncovered_code = missing
+            covered_linenumbers = sorted(set(executable) - set(missing))
+            out.write(" (puthash \"%s\" '((%s) (%s) (%s)) results)\n"
+                      % (f,
+                         " ".join([str(ln) for ln in sorted(code_linenumbers)]),
+                         " ".join([str(ln) for ln in sorted(covered_linenumbers)]),
+                         " ".join([str(ln) for ln in sorted(uncovered_code)]),
+                         ))
+        out.write(" results)\n")
+        out.close()
+
+def main():
+    c = coverage()
+    c.load()
+    ElispReporter(c).report()
+
+if __name__ == '__main__':
+    main()
+
+
diff --git a/misc/coverage2text.py b/misc/coverage2text.py
new file mode 100755
index 00000000..f91e25b5
--- /dev/null
+++ b/misc/coverage2text.py
@@ -0,0 +1,116 @@
+
+import sys
+from coverage import coverage
+from coverage.results import Numbers
+from coverage.summary import SummaryReporter
+from twisted.python import usage
+
+# this is an adaptation of the code behind "coverage report", modified to
+# display+sortby "lines uncovered", which (IMHO) is more important of a
+# metric than lines covered or percentage covered. Concentrating on the files
+# with the most uncovered lines encourages getting the tree and test suite
+# into a state that provides full line-coverage on all files.
+
+# much of this code was adapted from coverage/summary.py in the 'coverage'
+# distribution, and is used under their BSD license.
+
+class Options(usage.Options):
+    optParameters = [
+        ("sortby", "s", "uncovered", "how to sort: uncovered, covered, name"),
+        ]
+
+class MyReporter(SummaryReporter):
+    def report(self, outfile=None, sortby="uncovered"):
+        self.find_code_units(None, ["/System", "/Library", "/usr/lib",
+                                    "support/lib", "src/allmydata/test"])
+
+        # Prepare the formatting strings
+        max_name = max([len(cu.name) for cu in self.code_units] + [5])
+        fmt_name = "%%- %ds  " % max_name
+        fmt_err = "%s   %s: %s\n"
+        header1 = (fmt_name % ""    ) + "     Statements    "
+        header2 = (fmt_name % "Name") + " Uncovered  Covered"
+        fmt_coverage = fmt_name + "%9d  %7d "
+        if self.branches:
+            header1 += "   Branches   "
+            header2 += " Found  Excutd"
+            fmt_coverage += " %6d %6d"
+        header1 += "  Percent"
+        header2 += "  Covered"
+        fmt_coverage += " %7d%%"
+        if self.show_missing:
+            header1 += "          "
+            header2 += "   Missing"
+            fmt_coverage += "   %s"
+        rule = "-" * len(header1) + "\n"
+        header1 += "\n"
+        header2 += "\n"
+        fmt_coverage += "\n"
+
+        if not outfile:
+            outfile = sys.stdout
+
+        # Write the header
+        outfile.write(header1)
+        outfile.write(header2)
+        outfile.write(rule)
+
+        total = Numbers()
+        total_uncovered = 0
+
+        lines = []
+        for cu in self.code_units:
+            try:
+                analysis = self.coverage._analyze(cu)
+                nums = analysis.numbers
+                uncovered = nums.n_statements - nums.n_executed
+                total_uncovered += uncovered
+                args = (cu.name, uncovered, nums.n_executed)
+                if self.branches:
+                    args += (nums.n_branches, nums.n_executed_branches)
+                args += (nums.pc_covered,)
+                if self.show_missing:
+                    args += (analysis.missing_formatted(),)
+                if sortby == "covered":
+                    sortkey = nums.pc_covered
+                elif sortby == "uncovered":
+                    sortkey = uncovered
+                else:
+                    sortkey = cu.name
+                lines.append((sortkey, fmt_coverage % args))
+                total += nums
+            except KeyboardInterrupt:                       # pragma: no cover
+                raise
+            except:
+                if not self.ignore_errors:
+                    typ, msg = sys.exc_info()[:2]
+                    outfile.write(fmt_err % (cu.name, typ.__name__, msg))
+        lines.sort()
+        if sortby in ("uncovered", "covered"):
+            lines.reverse()
+        for sortkey,line in lines:
+            outfile.write(line)
+
+        if total.n_files > 1:
+            outfile.write(rule)
+            args = ("TOTAL", total_uncovered, total.n_executed)
+            if self.branches:
+                args += (total.n_branches, total.n_executed_branches)
+            args += (total.pc_covered,)
+            if self.show_missing:
+                args += ("",)
+            outfile.write(fmt_coverage % args)
+
+def report(o):
+    c = coverage()
+    c.load()
+    r = MyReporter(c, show_missing=False, ignore_errors=False)
+    r.report(sortby=o['sortby'])
+
+if __name__ == '__main__':
+    o = Options()
+    o.parseOptions()
+    report(o)
+
+
+
diff --git a/src/allmydata/test/trial_coverage.py b/src/allmydata/test/trial_coverage.py
new file mode 100644
index 00000000..47569da1
--- /dev/null
+++ b/src/allmydata/test/trial_coverage.py
@@ -0,0 +1,110 @@
+
+"""A Trial IReporter plugin that gathers coverage.py code-coverage information.
+
+Once this plugin is installed, trial can be invoked a new --reporter option:
+
+  trial --reporter-bwverbose-coverage ARGS
+
+Once such a test run has finished, there will be a .coverage file in the
+top-level directory. This file can be turned into a directory of .html files
+(with index.html as the starting point) by running:
+
+ coverage html -d OUTPUTDIR --omit=PREFIX1,PREFIX2,..
+
+The 'coverage' tool thinks in terms of absolute filenames. 'coverage' doesn't
+record data for files that come with Python, but it does record data for all
+the various site-package directories. To show only information for Tahoe
+source code files, you should provide --omit prefixes for everything else.
+This probably means something like:
+
+  --omit=/System/,/Library/,support/,src/allmydata/test/
+
+Before using this, you need to install the 'coverage' package, which will
+provide an executable tool named 'coverage' (as well as an importable
+library). 'coverage report' will produce a basic text summary of the coverage
+data. Our 'misc/coverage2text.py' tool produces a slightly more useful
+summary, and 'misc/coverage2html.py' will produce a more useful HTML report.
+
+"""
+
+from twisted.trial.reporter import TreeReporter, VerboseTextReporter
+
+# These plugins are registered via twisted/plugins/allmydata_trial.py . See
+# the notes there for an explanation of how that works.
+
+# Some notes about how trial Reporters are used:
+# * Reporters don't really get told about the suite starting and stopping.
+# * The Reporter class is imported before the test classes are.
+# * The test classes are imported before the Reporter is created. To get
+#   control earlier than that requires modifying twisted/scripts/trial.py
+# * Then Reporter.__init__ is called.
+# * Then tests run, calling things like write() and addSuccess(). Each test is
+#   framed by a startTest/stopTest call.
+# * Then the results are emitted, calling things like printErrors,
+#   printSummary, and wasSuccessful.
+# So for code-coverage (not including import), start in __init__ and finish
+# in printSummary. To include import, we have to start in our own import and
+# finish in printSummary.
+
+import coverage
+cov = coverage.coverage()
+cov.start()
+
+
+class CoverageTextReporter(VerboseTextReporter):
+    def __init__(self, *args, **kwargs):
+        VerboseTextReporter.__init__(self, *args, **kwargs)
+
+    def stop_coverage(self):
+        cov.stop()
+        cov.save()
+        print "Coverage results written to .coverage"
+    def printSummary(self):
+        # for twisted-2.5.x
+        self.stop_coverage()
+        return VerboseTextReporter.printSummary(self)
+    def done(self):
+        # for twisted-8.x
+        self.stop_coverage()
+        return VerboseTextReporter.done(self)
+
+class sample_Reporter(object):
+    # this class, used as a reporter on a fully-passing test suite, doesn't
+    # trigger exceptions. So it is a guide to what methods are invoked on a
+    # Reporter.
+    def __init__(self, *args, **kwargs):
+        print "START HERE"
+        self.r = TreeReporter(*args, **kwargs)
+        self.shouldStop = self.r.shouldStop
+        self.separator = self.r.separator
+        self.testsRun = self.r.testsRun
+        self._starting2 = False
+
+    def write(self, *args):
+        if not self._starting2:
+            self._starting2 = True
+            print "FIRST WRITE"
+        return self.r.write(*args)
+
+    def startTest(self, *args, **kwargs):
+        return self.r.startTest(*args, **kwargs)
+
+    def stopTest(self, *args, **kwargs):
+        return self.r.stopTest(*args, **kwargs)
+
+    def addSuccess(self, *args, **kwargs):
+        return self.r.addSuccess(*args, **kwargs)
+
+    def printErrors(self, *args, **kwargs):
+        return self.r.printErrors(*args, **kwargs)
+
+    def writeln(self, *args, **kwargs):
+        return self.r.writeln(*args, **kwargs)
+
+    def printSummary(self, *args, **kwargs):
+        print "PRINT SUMMARY"
+        return self.r.printSummary(*args, **kwargs)
+
+    def wasSuccessful(self, *args, **kwargs):
+        return self.r.wasSuccessful(*args, **kwargs)
+
diff --git a/src/allmydata/test/trial_figleaf.py b/src/allmydata/test/trial_figleaf.py
deleted file mode 100644
index 49a4e689..00000000
--- a/src/allmydata/test/trial_figleaf.py
+++ /dev/null
@@ -1,139 +0,0 @@
-
-"""A Trial IReporter plugin that gathers figleaf code-coverage information.
-
-Once this plugin is installed, trial can be invoked with one of two new
---reporter options:
-
-  trial --reporter=verbose-figleaf ARGS
-  trial --reporter-bwverbose-figleaf ARGS
-
-Once such a test run has finished, there will be a .figleaf file in the
-top-level directory. This file can be turned into a directory of .html files
-(with index.html as the starting point) by running:
-
- figleaf2html -d OUTPUTDIR [-x EXCLUDEFILE]
-
-Figleaf thinks of everyting in terms of absolute filenames rather than
-modules. The EXCLUDEFILE may be necessary to keep it from providing reports
-on non-Code-Under-Test files that live in unusual locations. In particular,
-if you use extra PYTHONPATH arguments to point at some alternate version of
-an upstream library (like Twisted), or if something like debian's
-python-support puts symlinks to .py files in sys.path but not the .py files
-themselves, figleaf will present coverage information on both of these. The
-EXCLUDEFILE option might help to inhibit these.
-
-Other figleaf problems:
-
- the annotated code files are written to BASENAME(file).html, which results
- in collisions between similarly-named source files.
-
- The line-wise coverage information isn't quite right. Blank lines are
- counted as unreached code, lambdas aren't quite right, and some multiline
- comments (docstrings?) aren't quite right.
-
-"""
-
-from twisted.trial.reporter import TreeReporter, VerboseTextReporter
-
-# These plugins are registered via twisted/plugins/allmydata_trial.py . See
-# the notes there for an explanation of how that works.
-
-
-
-# Reporters don't really get told about the suite starting and stopping.
-
-# The Reporter class is imported before the test classes are.
-
-# The test classes are imported before the Reporter is created. To get
-# control earlier than that requires modifying twisted/scripts/trial.py .
-
-# Then Reporter.__init__ is called.
-
-# Then tests run, calling things like write() and addSuccess(). Each test is
-# framed by a startTest/stopTest call.
-
-# Then the results are emitted, calling things like printErrors,
-# printSummary, and wasSuccessful.
-
-# So for code-coverage (not including import), start in __init__ and finish
-# in printSummary. To include import, we have to start in our own import and
-# finish in printSummary.
-
-import figleaf
-figleaf.start()
-
-
-class FigleafReporter(TreeReporter):
-    def __init__(self, *args, **kwargs):
-        TreeReporter.__init__(self, *args, **kwargs)
-
-    def stop_figleaf(self):
-        figleaf.stop()
-        figleaf.write_coverage(".figleaf")
-        print "Figleaf results written to .figleaf"
-    def printSummary(self):
-        # for twisted-2.5.x
-        self.stop_figleaf()
-        return TreeReporter.printSummary(self)
-    def done(self):
-        # for twisted-8.x
-        self.stop_figleaf()
-        return TreeReporter.done(self)
-
-class FigleafTextReporter(VerboseTextReporter):
-    def __init__(self, *args, **kwargs):
-        VerboseTextReporter.__init__(self, *args, **kwargs)
-
-    def stop_figleaf(self):
-        figleaf.stop()
-        figleaf.write_coverage(".figleaf")
-        print "Figleaf results written to .figleaf"
-    def printSummary(self):
-        # for twisted-2.5.x
-        self.stop_figleaf()
-        return VerboseTextReporter.printSummary(self)
-    def done(self):
-        # for twisted-8.x
-        self.stop_figleaf()
-        return VerboseTextReporter.done(self)
-
-class not_FigleafReporter(object):
-    # this class, used as a reporter on a fully-passing test suite, doesn't
-    # trigger exceptions. So it is a guide to what methods are invoked on a
-    # Reporter.
-    def __init__(self, *args, **kwargs):
-        print "FIGLEAF HERE"
-        self.r = TreeReporter(*args, **kwargs)
-        self.shouldStop = self.r.shouldStop
-        self.separator = self.r.separator
-        self.testsRun = self.r.testsRun
-        self._starting2 = False
-
-    def write(self, *args):
-        if not self._starting2:
-            self._starting2 = True
-            print "FIRST WRITE"
-        return self.r.write(*args)
-
-    def startTest(self, *args, **kwargs):
-        return self.r.startTest(*args, **kwargs)
-
-    def stopTest(self, *args, **kwargs):
-        return self.r.stopTest(*args, **kwargs)
-
-    def addSuccess(self, *args, **kwargs):
-        return self.r.addSuccess(*args, **kwargs)
-
-    def printErrors(self, *args, **kwargs):
-        return self.r.printErrors(*args, **kwargs)
-
-    def writeln(self, *args, **kwargs):
-        return self.r.writeln(*args, **kwargs)
-
-    def printSummary(self, *args, **kwargs):
-        print "PRINT SUMMARY"
-        return self.r.printSummary(*args, **kwargs)
-
-    def wasSuccessful(self, *args, **kwargs):
-        return self.r.wasSuccessful(*args, **kwargs)
-
diff --git a/twisted/plugins/allmydata_trial.py b/twisted/plugins/allmydata_trial.py
index 275bbb24..11cbeade 100644
--- a/twisted/plugins/allmydata_trial.py
+++ b/twisted/plugins/allmydata_trial.py
@@ -4,18 +4,18 @@ from zope.interface import implements
 from twisted.trial.itrial import IReporter
 from twisted.plugin import IPlugin
 
-# register a plugin that can create our FigleafReporter. The reporter itself
-# lives in a separate place
-
-# note that this .py file is *not* in a package: there is no __init__.py in
-# our parent directory. This is important, because otherwise ours would fight
-# with Twisted's. When trial looks for plugins, it merely executes all the
-# *.py files it finds in any twisted/plugins/ subdirectories of anything on
-# sys.path . The namespace that results from executing these .py files is
-# examined for instances which provide both IPlugin and the target interface
-# (in this case, trial is looking for IReporter instances). Each such
-# instance tells the application how to create a plugin by naming the module
-# and class that should be instantiated.
+# register a plugin that can create our CoverageReporter. The reporter itself
+# lives separately, in src/allmydata/test/trial_figleaf.py
+
+# note that this allmydata_trial.py file is *not* in a package: there is no
+# __init__.py in our parent directory. This is important, because otherwise
+# ours would fight with Twisted's. When trial looks for plugins, it merely
+# executes all the *.py files it finds in any twisted/plugins/ subdirectories
+# of anything on sys.path . The namespace that results from executing these
+# .py files is examined for instances which provide both IPlugin and the
+# target interface (in this case, trial is looking for IReporter instances).
+# Each such instance tells the application how to create a plugin by naming
+# the module and class that should be instantiated.
 
 # When installing our package via setup.py, arrange for this file to be
 # installed to the system-wide twisted/plugins/ directory.
@@ -32,17 +32,10 @@ class _Reporter(object):
         self.klass = klass
 
 
-fig = _Reporter("Figleaf Code-Coverage Reporter",
-                "allmydata.test.trial_figleaf",
-                description="verbose color output (with figleaf coverage)",
-                longOpt="verbose-figleaf",
-                shortOpt="f",
-                klass="FigleafReporter")
-
-bwfig = _Reporter("Figleaf Code-Coverage Reporter (colorless)",
-                  "allmydata.test.trial_figleaf",
-                  description="Colorless verbose output (with figleaf coverage)",
-                  longOpt="bwverbose-figleaf",
+bwcov = _Reporter("Code-Coverage Reporter (colorless)",
+                  "allmydata.test.trial_coverage",
+                  description="Colorless verbose output (with 'coverage' coverage)",
+                  longOpt="bwverbose-coverage",
                   shortOpt=None,
-                  klass="FigleafTextReporter")
+                  klass="CoverageTextReporter")
 
-- 
2.45.2