Ticket #930: 930.diff.txt

File 930.diff.txt, 32.8 KB (added by warner, at 2010-01-28T18:38:17Z)
Line 
1diff --git a/.darcs-boringfile b/.darcs-boringfile
2index 09913bd..908d6c0 100644
3--- a/.darcs-boringfile
4+++ b/.darcs-boringfile
5@@ -52,10 +52,10 @@
6 ^build($|/)
7 ^build-stamp$
8 ^python-build-stamp-2.[45]$
9-^\.figleaf$
10+^\.coverage$
11 ^coverage-html($|/)
12 ^twisted/plugins/dropin\.cache$
13-^\.figleaf\.el$
14+^\.coverage\.el$
15 ^_test_memory($|/)
16 
17 # _version.py is generated at build time, and never checked in
18diff --git a/Makefile b/Makefile
19index e220a51..d61b8ae 100644
20--- a/Makefile
21+++ b/Makefile
22@@ -81,8 +81,8 @@ endif
23 
24 # TESTING
25 
26-.PHONY: signal-error-deps test test-figleaf quicktest quicktest-figleaf
27-.PHONY: figleaf-output get-old-figleaf-coverage figleaf-delta-output
28+.PHONY: signal-error-deps test test-coverage quicktest quicktest-coverage
29+.PHONY: coverage-output get-old-coverage-coverage coverage-delta-output
30 
31 
32 signal-error-deps:
33@@ -114,41 +114,49 @@ test: build src/allmydata/_version.py
34 fuse-test: .built .checked-deps
35        $(RUNPP) -d contrib/fuse -p -c runtests.py
36 
37-test-figleaf: build src/allmydata/_version.py
38-       rm -f .figleaf
39-       $(PYTHON) setup.py trial --reporter=bwverbose-figleaf -s $(TEST)
40+test-coverage: build src/allmydata/_version.py
41+       rm -f .coverage
42+       $(PYTHON) setup.py trial --reporter=bwverbose-coverage -s $(TEST)
43 
44 quicktest:
45        $(PYTHON) misc/run-with-pythonpath.py trial $(TRIALARGS) $(TEST)
46 
47-quicktest-figleaf:
48-       rm -f .figleaf
49-       $(PYTHON) misc/run-with-pythonpath.py trial --reporter=bwverbose-figleaf $(TEST)
50+quicktest-coverage:
51+       rm -f .coverage
52+       $(PYTHON) misc/run-with-pythonpath.py trial --reporter=bwverbose-coverage $(TEST)
53+# takes 304s with .coverage, 239s without
54 
55-figleaf-output:
56-       $(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes"
57-       cp .figleaf coverage-html/figleaf.pickle
58-       @echo "now point your browser at coverage-html/index.html"
59+COVERAGE_OMIT = --omit /System,/Library,/usr/lib,src/allmydata/test,support
60 
61-# use these two targets to compare this coverage against the previous run.
62-# The deltas only work if the old test was run in the same directory, since
63-# it compares absolute filenames.
64-get-old-figleaf-coverage:
65-       wget --progress=dot -O old.figleaf http://allmydata.org/tahoe-figleaf/current/figleaf.pickle
66+# these are like 'coverage report' and 'coverage html', but more useful
67+coverage-output-text:
68+       $(PYTHON) misc/coverage2text.py
69 
70-figleaf-delta-output:
71-       $(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes -o old.figleaf"
72-       cp .figleaf coverage-html/figleaf.pickle
73+coverage-output:
74+       rm -rf coverage-html
75+       coverage html -d coverage-html $(COVERAGE_OMIT)
76+       cp .coverage coverage-html/coverage.data
77        @echo "now point your browser at coverage-html/index.html"
78 
79-# after doing test-figleaf and figleaf-output, point your browser at
80-# coverage-html/index.html
81+## use these two targets to compare this coverage against the previous run.
82+## The deltas only work if the old test was run in the same directory, since
83+## it compares absolute filenames.
84+#get-old-figleaf-coverage:
85+#      wget --progress=dot -O old.figleaf http://allmydata.org/tahoe-figleaf/current/figleaf.pickle
86+#
87+#figleaf-delta-output:
88+#      $(RUNPP) -p -c "misc/figleaf2html -d coverage-html -r src -x misc/figleaf.excludes -o old.figleaf"
89+#      cp .figleaf coverage-html/figleaf.pickle
90+#      @echo "now point your browser at coverage-html/index.html"
91 
92-.PHONY: upload-figleaf .figleaf.el pyflakes count-lines
93+.PHONY: upload-coverage .coverage.el pyflakes count-lines
94 .PHONY: check-memory check-memory-once check-speed check-grid
95 .PHONY: repl test-darcs-boringfile test-clean clean find-trailing-spaces
96 
97-# 'upload-figleaf' is meant to be run with an UPLOAD_TARGET=host:/dir setting
98+.coverage.el: .coverage
99+       $(PYTHON) misc/coverage2el.py
100+
101+# 'upload-coverage' is meant to be run with an UPLOAD_TARGET=host:/dir setting
102 ifdef UPLOAD_TARGET
103 
104 ifndef UPLOAD_HOST
105@@ -158,17 +166,15 @@ ifndef COVERAGEDIR
106 $(error COVERAGEDIR must be set when using UPLOAD_TARGET)
107 endif
108 
109-upload-figleaf:
110+upload-coverage:
111        rsync -a coverage-html/ $(UPLOAD_TARGET)
112-       ssh $(UPLOAD_HOST) make update-tahoe-figleaf COVERAGEDIR=$(COVERAGEDIR)
113+       ssh $(UPLOAD_HOST) make update-tahoe-coverage COVERAGEDIR=$(COVERAGEDIR)
114 else
115-upload-figleaf:
116+upload-coverage:
117        echo "this target is meant to be run with UPLOAD_TARGET=host:/path/"
118        false
119 endif
120 
121-.figleaf.el: .figleaf
122-       $(RUNPP) -p -c "misc/figleaf2el.py .figleaf src"
123 
124 pyflakes:
125        $(PYTHON) -OOu `which pyflakes` src/allmydata |sort |uniq
126diff --git a/misc/coverage.el b/misc/coverage.el
127new file mode 100644
128index 0000000..64e7134
129--- /dev/null
130+++ b/misc/coverage.el
131@@ -0,0 +1,120 @@
132+
133+(defvar coverage-annotation-file ".coverage.el")
134+(defvar coverage-annotations nil)
135+
136+(defun find-coverage-annotation-file ()
137+  (let ((dir (file-name-directory buffer-file-name))
138+        (olddir "/"))
139+    (while (and (not (equal dir olddir))
140+                (not (file-regular-p (concat dir coverage-annotation-file))))
141+      (setq olddir dir
142+            dir (file-name-directory (directory-file-name dir))))
143+    (and (not (equal dir olddir)) (concat dir coverage-annotation-file))
144+))
145+
146+(defun load-coverage-annotations ()
147+  (let* ((annotation-file (find-coverage-annotation-file))
148+         (coverage
149+          (with-temp-buffer
150+            (insert-file-contents annotation-file)
151+            (let ((form (read (current-buffer))))
152+              (eval form)))))
153+    (setq coverage-annotations coverage)
154+    coverage
155+    ))
156+
157+(defun coverage-unannotate ()
158+  (save-excursion
159+    (dolist (ov (overlays-in (point-min) (point-max)))
160+      (delete-overlay ov))
161+    (setq coverage-this-buffer-is-annotated nil)
162+    (message "Removed annotations")
163+))
164+
165+;; in emacs22, it will be possible to put the annotations in the fringe. Set
166+;; a display property for one of the characters in the line, using
167+;; (right-fringe BITMAP FACE), where BITMAP should probably be right-triangle
168+;; or so, and FACE should probably be '(:foreground "red"). We can also
169+;; create new bitmaps, with faces. To do tartans will require a lot of
170+;; bitmaps, and you've only got about 8 pixels to work with.
171+
172+;; unfortunately emacs21 gives us less control over the fringe. We can use
173+;; overlays to put letters on the left or right margins (in the text area,
174+;; overriding actual program text), and to modify the text being displayed
175+;; (by changing its background color, or adding a box around each word).
176+
177+(defun coverage-annotate (show-code)
178+  (let ((allcoverage (load-coverage-annotations))
179+        (filename-key buffer-file-truename)
180+        thiscoverage code-lines covered-lines uncovered-code-lines
181+        )
182+    (while (and (not (gethash filename-key allcoverage nil))
183+                (string-match "/" filename-key))
184+      ;; eat everything up to and including the first slash, then look again
185+      (setq filename-key (substring filename-key
186+                                    (+ 1 (string-match "/" filename-key)))))
187+    (setq thiscoverage (gethash filename-key allcoverage nil))
188+    (if thiscoverage
189+        (progn
190+          (setq coverage-this-buffer-is-annotated t)
191+          (setq code-lines (nth 0 thiscoverage)
192+                covered-lines (nth 1 thiscoverage)
193+                uncovered-code-lines (nth 2 thiscoverage)
194+                )
195+
196+          (save-excursion
197+            (dolist (ov (overlays-in (point-min) (point-max)))
198+              (delete-overlay ov))
199+            (if show-code
200+                (dolist (line code-lines)
201+                  (goto-line line)
202+                  ;;(add-text-properties (point) (line-end-position) '(face bold) )
203+                  (overlay-put (make-overlay (point) (line-end-position))
204+                                        ;'before-string "C"
205+                                        ;'face '(background-color . "green")
206+                               'face '(:background "dark green")
207+                               )
208+                  ))
209+            (dolist (line uncovered-code-lines)
210+              (goto-line line)
211+              (overlay-put (make-overlay (point) (line-end-position))
212+                                        ;'before-string "D"
213+                                        ;'face '(:background "blue")
214+                                        ;'face '(:underline "blue")
215+                           'face '(:box "red")
216+                           )
217+              )
218+            (message "Added annotations")
219+            )
220+          )
221+      (message "unable to find coverage for this file"))
222+))
223+
224+(defun coverage-toggle-annotations (show-code)
225+  (interactive "P")
226+  (if coverage-this-buffer-is-annotated
227+      (coverage-unannotate)
228+    (coverage-annotate show-code))
229+)
230+
231+
232+(setq coverage-this-buffer-is-annotated nil)
233+(make-variable-buffer-local 'coverage-this-buffer-is-annotated)
234+
235+(define-minor-mode coverage-annotation-minor-mode
236+  "Minor mode to annotate code-coverage information"
237+  nil
238+  " CA"
239+  '(
240+    ("\C-c\C-a" . coverage-toggle-annotations)
241+    )
242+
243+  () ; forms run on mode entry/exit
244+)
245+
246+(defun maybe-enable-coverage-mode ()
247+  (if (string-match "/src/allmydata/" (buffer-file-name))
248+      (coverage-annotation-minor-mode t)
249+    ))
250+
251+(add-hook 'python-mode-hook 'maybe-enable-coverage-mode)
252diff --git a/misc/coverage2el.py b/misc/coverage2el.py
253new file mode 100755
254index 0000000..ed94bd0
255--- /dev/null
256+++ b/misc/coverage2el.py
257@@ -0,0 +1,45 @@
258+
259+from coverage import coverage, summary
260+
261+class ElispReporter(summary.SummaryReporter):
262+    def report(self):
263+        self.find_code_units(None, ["/System", "/Library", "/usr/lib",
264+                                    "support/lib", "src/allmydata/test"])
265+
266+        out = open(".coverage.el", "w")
267+        out.write("""
268+;; This is an elisp-readable form of the figleaf coverage data. It defines a
269+;; single top-level hash table in which the key is an asolute pathname, and
270+;; the value is a three-element list. The first element of this list is a
271+;; list of line numbers that represent actual code statements. The second is
272+;; a list of line numbers for lines which got used during the unit test. The
273+;; third is a list of line numbers for code lines that were not covered
274+;; (since 'code' and 'covered' start as sets, this last list is equal to
275+;; 'code - covered').
276+
277+    """)
278+        out.write("(let ((results (make-hash-table :test 'equal)))\n")
279+        for cu in self.code_units:
280+            f = cu.filename
281+            (fn, executable, missing, mf) = self.coverage.analysis(cu)
282+            code_linenumbers = executable
283+            uncovered_code = missing
284+            covered_linenumbers = sorted(set(executable) - set(missing))
285+            out.write(" (puthash \"%s\" '((%s) (%s) (%s)) results)\n"
286+                      % (f,
287+                         " ".join([str(ln) for ln in sorted(code_linenumbers)]),
288+                         " ".join([str(ln) for ln in sorted(covered_linenumbers)]),
289+                         " ".join([str(ln) for ln in sorted(uncovered_code)]),
290+                         ))
291+        out.write(" results)\n")
292+        out.close()
293+
294+def main():
295+    c = coverage()
296+    c.load()
297+    ElispReporter(c).report()
298+
299+if __name__ == '__main__':
300+    main()
301+
302+
303diff --git a/misc/coverage2text.py b/misc/coverage2text.py
304new file mode 100755
305index 0000000..f91e25b
306--- /dev/null
307+++ b/misc/coverage2text.py
308@@ -0,0 +1,116 @@
309+
310+import sys
311+from coverage import coverage
312+from coverage.results import Numbers
313+from coverage.summary import SummaryReporter
314+from twisted.python import usage
315+
316+# this is an adaptation of the code behind "coverage report", modified to
317+# display+sortby "lines uncovered", which (IMHO) is more important of a
318+# metric than lines covered or percentage covered. Concentrating on the files
319+# with the most uncovered lines encourages getting the tree and test suite
320+# into a state that provides full line-coverage on all files.
321+
322+# much of this code was adapted from coverage/summary.py in the 'coverage'
323+# distribution, and is used under their BSD license.
324+
325+class Options(usage.Options):
326+    optParameters = [
327+        ("sortby", "s", "uncovered", "how to sort: uncovered, covered, name"),
328+        ]
329+
330+class MyReporter(SummaryReporter):
331+    def report(self, outfile=None, sortby="uncovered"):
332+        self.find_code_units(None, ["/System", "/Library", "/usr/lib",
333+                                    "support/lib", "src/allmydata/test"])
334+
335+        # Prepare the formatting strings
336+        max_name = max([len(cu.name) for cu in self.code_units] + [5])
337+        fmt_name = "%%- %ds  " % max_name
338+        fmt_err = "%s   %s: %s\n"
339+        header1 = (fmt_name % ""    ) + "     Statements    "
340+        header2 = (fmt_name % "Name") + " Uncovered  Covered"
341+        fmt_coverage = fmt_name + "%9d  %7d "
342+        if self.branches:
343+            header1 += "   Branches   "
344+            header2 += " Found  Excutd"
345+            fmt_coverage += " %6d %6d"
346+        header1 += "  Percent"
347+        header2 += "  Covered"
348+        fmt_coverage += " %7d%%"
349+        if self.show_missing:
350+            header1 += "          "
351+            header2 += "   Missing"
352+            fmt_coverage += "   %s"
353+        rule = "-" * len(header1) + "\n"
354+        header1 += "\n"
355+        header2 += "\n"
356+        fmt_coverage += "\n"
357+
358+        if not outfile:
359+            outfile = sys.stdout
360+
361+        # Write the header
362+        outfile.write(header1)
363+        outfile.write(header2)
364+        outfile.write(rule)
365+
366+        total = Numbers()
367+        total_uncovered = 0
368+
369+        lines = []
370+        for cu in self.code_units:
371+            try:
372+                analysis = self.coverage._analyze(cu)
373+                nums = analysis.numbers
374+                uncovered = nums.n_statements - nums.n_executed
375+                total_uncovered += uncovered
376+                args = (cu.name, uncovered, nums.n_executed)
377+                if self.branches:
378+                    args += (nums.n_branches, nums.n_executed_branches)
379+                args += (nums.pc_covered,)
380+                if self.show_missing:
381+                    args += (analysis.missing_formatted(),)
382+                if sortby == "covered":
383+                    sortkey = nums.pc_covered
384+                elif sortby == "uncovered":
385+                    sortkey = uncovered
386+                else:
387+                    sortkey = cu.name
388+                lines.append((sortkey, fmt_coverage % args))
389+                total += nums
390+            except KeyboardInterrupt:                       # pragma: no cover
391+                raise
392+            except:
393+                if not self.ignore_errors:
394+                    typ, msg = sys.exc_info()[:2]
395+                    outfile.write(fmt_err % (cu.name, typ.__name__, msg))
396+        lines.sort()
397+        if sortby in ("uncovered", "covered"):
398+            lines.reverse()
399+        for sortkey,line in lines:
400+            outfile.write(line)
401+
402+        if total.n_files > 1:
403+            outfile.write(rule)
404+            args = ("TOTAL", total_uncovered, total.n_executed)
405+            if self.branches:
406+                args += (total.n_branches, total.n_executed_branches)
407+            args += (total.pc_covered,)
408+            if self.show_missing:
409+                args += ("",)
410+            outfile.write(fmt_coverage % args)
411+
412+def report(o):
413+    c = coverage()
414+    c.load()
415+    r = MyReporter(c, show_missing=False, ignore_errors=False)
416+    r.report(sortby=o['sortby'])
417+
418+if __name__ == '__main__':
419+    o = Options()
420+    o.parseOptions()
421+    report(o)
422+
423+
424+
425diff --git a/misc/figleaf.el b/misc/figleaf.el
426deleted file mode 100644
427index fef42e0..0000000
428--- a/misc/figleaf.el
429+++ /dev/null
430@@ -1,140 +0,0 @@
431-
432-;(require 'gnus-start)
433-
434-; (defun gnus-load (file)
435-;   "Load FILE, but in such a way that read errors can be reported."
436-;   (with-temp-buffer
437-;     (insert-file-contents file)
438-;     (while (not (eobp))
439-;       (condition-case type
440-;        (let ((form (read (current-buffer))))
441-;          (eval form))
442-;      (error
443-;       (unless (eq (car type) 'end-of-file)
444-;         (let ((error (format "Error in %s line %d" file
445-;                              (count-lines (point-min) (point)))))
446-;           (ding)
447-;           (unless (gnus-yes-or-no-p (concat error "; continue? "))
448-;             (error "%s" error)))))))))
449-
450-(defvar figleaf-annotation-file ".figleaf.el")
451-(defvar figleaf-annotations nil)
452-
453-(defun find-figleaf-annotation-file ()
454-  (let ((dir (file-name-directory buffer-file-name))
455-        (olddir "/"))
456-    (while (and (not (equal dir olddir))
457-                (not (file-regular-p (concat dir figleaf-annotation-file))))
458-      (setq olddir dir
459-            dir (file-name-directory (directory-file-name dir))))
460-    (and (not (equal dir olddir)) (concat dir figleaf-annotation-file))
461-))
462-
463-(defun load-figleaf-annotations ()
464-  (let* ((annotation-file (find-figleaf-annotation-file))
465-         (coverage
466-          (with-temp-buffer
467-            (insert-file-contents annotation-file)
468-            (let ((form (read (current-buffer))))
469-              (eval form)))))
470-    (setq figleaf-annotations coverage)
471-    coverage
472-    ))
473-
474-(defun figleaf-unannotate ()
475-  (interactive)
476-  (save-excursion
477-    (dolist (ov (overlays-in (point-min) (point-max)))
478-      (delete-overlay ov))
479-    (setq figleaf-this-buffer-is-annotated nil)
480-    (message "Removed annotations")
481-))
482-
483-;; in emacs22, it will be possible to put the annotations in the fringe. Set
484-;; a display property for one of the characters in the line, using
485-;; (right-fringe BITMAP FACE), where BITMAP should probably be right-triangle
486-;; or so, and FACE should probably be '(:foreground "red"). We can also
487-;; create new bitmaps, with faces. To do tartans will require a lot of
488-;; bitmaps, and you've only got about 8 pixels to work with.
489-
490-;; unfortunately emacs21 gives us less control over the fringe. We can use
491-;; overlays to put letters on the left or right margins (in the text area,
492-;; overriding actual program text), and to modify the text being displayed
493-;; (by changing its background color, or adding a box around each word).
494-
495-(defun figleaf-annotate (&optional show-code)
496-  (interactive "P")
497-  (let ((allcoverage (load-figleaf-annotations))
498-        (filename-key buffer-file-name)
499-        thiscoverage code-lines covered-lines uncovered-code-lines
500-        )
501-    (while (and (not (gethash filename-key allcoverage nil))
502-                (string-match "/" filename-key))
503-      ;; eat everything up to and including the first slash, then look again
504-      (setq filename-key (substring filename-key
505-                                    (+ 1 (string-match "/" filename-key)))))
506-    (setq thiscoverage (gethash filename-key allcoverage nil))
507-    (if thiscoverage
508-        (progn
509-          (setq figleaf-this-buffer-is-annotated t)
510-          (setq code-lines (nth 0 thiscoverage)
511-                covered-lines (nth 1 thiscoverage)
512-                uncovered-code-lines (nth 2 thiscoverage)
513-                )
514-
515-          (save-excursion
516-            (dolist (ov (overlays-in (point-min) (point-max)))
517-              (delete-overlay ov))
518-            (if show-code
519-                (dolist (line code-lines)
520-                  (goto-line line)
521-                  ;;(add-text-properties (point) (line-end-position) '(face bold) )
522-                  (overlay-put (make-overlay (point) (line-end-position))
523-                                        ;'before-string "C"
524-                                        ;'face '(background-color . "green")
525-                               'face '(:background "dark green")
526-                               )
527-                  ))
528-            (dolist (line uncovered-code-lines)
529-              (goto-line line)
530-              (overlay-put (make-overlay (point) (line-end-position))
531-                                        ;'before-string "D"
532-                                        ;'face '(:background "blue")
533-                                        ;'face '(:underline "blue")
534-                           'face '(:box "red")
535-                           )
536-              )
537-            (message "Added annotations")
538-            )
539-          )
540-      (message "unable to find coverage for this file"))
541-))
542-
543-(defun figleaf-toggle-annotations (show-code)
544-  (interactive "P")
545-  (if figleaf-this-buffer-is-annotated
546-      (figleaf-unannotate)
547-    (figleaf-annotate show-code))
548-)
549-
550-
551-(setq figleaf-this-buffer-is-annotated nil)
552-(make-variable-buffer-local 'figleaf-this-buffer-is-annotated)
553-
554-(define-minor-mode figleaf-annotation-minor-mode
555-  "Minor mode to annotate code-coverage information"
556-  nil
557-  " FA"
558-  '(
559-    ("\C-c\C-a" . figleaf-toggle-annotations)
560-    )
561-
562-  () ; forms run on mode entry/exit
563-)
564-
565-(defun maybe-enable-figleaf-mode ()
566-  (if (string-match "/src/allmydata/" (buffer-file-name))
567-      (figleaf-annotation-minor-mode t)
568-    ))
569-
570-(add-hook 'python-mode-hook 'maybe-enable-figleaf-mode)
571diff --git a/src/allmydata/test/trial_coverage.py b/src/allmydata/test/trial_coverage.py
572new file mode 100644
573index 0000000..47569da
574--- /dev/null
575+++ b/src/allmydata/test/trial_coverage.py
576@@ -0,0 +1,110 @@
577+
578+"""A Trial IReporter plugin that gathers coverage.py code-coverage information.
579+
580+Once this plugin is installed, trial can be invoked a new --reporter option:
581+
582+  trial --reporter-bwverbose-coverage ARGS
583+
584+Once such a test run has finished, there will be a .coverage file in the
585+top-level directory. This file can be turned into a directory of .html files
586+(with index.html as the starting point) by running:
587+
588+ coverage html -d OUTPUTDIR --omit=PREFIX1,PREFIX2,..
589+
590+The 'coverage' tool thinks in terms of absolute filenames. 'coverage' doesn't
591+record data for files that come with Python, but it does record data for all
592+the various site-package directories. To show only information for Tahoe
593+source code files, you should provide --omit prefixes for everything else.
594+This probably means something like:
595+
596+  --omit=/System/,/Library/,support/,src/allmydata/test/
597+
598+Before using this, you need to install the 'coverage' package, which will
599+provide an executable tool named 'coverage' (as well as an importable
600+library). 'coverage report' will produce a basic text summary of the coverage
601+data. Our 'misc/coverage2text.py' tool produces a slightly more useful
602+summary, and 'misc/coverage2html.py' will produce a more useful HTML report.
603+
604+"""
605+
606+from twisted.trial.reporter import TreeReporter, VerboseTextReporter
607+
608+# These plugins are registered via twisted/plugins/allmydata_trial.py . See
609+# the notes there for an explanation of how that works.
610+
611+# Some notes about how trial Reporters are used:
612+# * Reporters don't really get told about the suite starting and stopping.
613+# * The Reporter class is imported before the test classes are.
614+# * The test classes are imported before the Reporter is created. To get
615+#   control earlier than that requires modifying twisted/scripts/trial.py
616+# * Then Reporter.__init__ is called.
617+# * Then tests run, calling things like write() and addSuccess(). Each test is
618+#   framed by a startTest/stopTest call.
619+# * Then the results are emitted, calling things like printErrors,
620+#   printSummary, and wasSuccessful.
621+# So for code-coverage (not including import), start in __init__ and finish
622+# in printSummary. To include import, we have to start in our own import and
623+# finish in printSummary.
624+
625+import coverage
626+cov = coverage.coverage()
627+cov.start()
628+
629+
630+class CoverageTextReporter(VerboseTextReporter):
631+    def __init__(self, *args, **kwargs):
632+        VerboseTextReporter.__init__(self, *args, **kwargs)
633+
634+    def stop_coverage(self):
635+        cov.stop()
636+        cov.save()
637+        print "Coverage results written to .coverage"
638+    def printSummary(self):
639+        # for twisted-2.5.x
640+        self.stop_coverage()
641+        return VerboseTextReporter.printSummary(self)
642+    def done(self):
643+        # for twisted-8.x
644+        self.stop_coverage()
645+        return VerboseTextReporter.done(self)
646+
647+class sample_Reporter(object):
648+    # this class, used as a reporter on a fully-passing test suite, doesn't
649+    # trigger exceptions. So it is a guide to what methods are invoked on a
650+    # Reporter.
651+    def __init__(self, *args, **kwargs):
652+        print "START HERE"
653+        self.r = TreeReporter(*args, **kwargs)
654+        self.shouldStop = self.r.shouldStop
655+        self.separator = self.r.separator
656+        self.testsRun = self.r.testsRun
657+        self._starting2 = False
658+
659+    def write(self, *args):
660+        if not self._starting2:
661+            self._starting2 = True
662+            print "FIRST WRITE"
663+        return self.r.write(*args)
664+
665+    def startTest(self, *args, **kwargs):
666+        return self.r.startTest(*args, **kwargs)
667+
668+    def stopTest(self, *args, **kwargs):
669+        return self.r.stopTest(*args, **kwargs)
670+
671+    def addSuccess(self, *args, **kwargs):
672+        return self.r.addSuccess(*args, **kwargs)
673+
674+    def printErrors(self, *args, **kwargs):
675+        return self.r.printErrors(*args, **kwargs)
676+
677+    def writeln(self, *args, **kwargs):
678+        return self.r.writeln(*args, **kwargs)
679+
680+    def printSummary(self, *args, **kwargs):
681+        print "PRINT SUMMARY"
682+        return self.r.printSummary(*args, **kwargs)
683+
684+    def wasSuccessful(self, *args, **kwargs):
685+        return self.r.wasSuccessful(*args, **kwargs)
686+
687diff --git a/src/allmydata/test/trial_figleaf.py b/src/allmydata/test/trial_figleaf.py
688deleted file mode 100644
689index 49a4e68..0000000
690--- a/src/allmydata/test/trial_figleaf.py
691+++ /dev/null
692@@ -1,139 +0,0 @@
693-
694-"""A Trial IReporter plugin that gathers figleaf code-coverage information.
695-
696-Once this plugin is installed, trial can be invoked with one of two new
697---reporter options:
698-
699-  trial --reporter=verbose-figleaf ARGS
700-  trial --reporter-bwverbose-figleaf ARGS
701-
702-Once such a test run has finished, there will be a .figleaf file in the
703-top-level directory. This file can be turned into a directory of .html files
704-(with index.html as the starting point) by running:
705-
706- figleaf2html -d OUTPUTDIR [-x EXCLUDEFILE]
707-
708-Figleaf thinks of everyting in terms of absolute filenames rather than
709-modules. The EXCLUDEFILE may be necessary to keep it from providing reports
710-on non-Code-Under-Test files that live in unusual locations. In particular,
711-if you use extra PYTHONPATH arguments to point at some alternate version of
712-an upstream library (like Twisted), or if something like debian's
713-python-support puts symlinks to .py files in sys.path but not the .py files
714-themselves, figleaf will present coverage information on both of these. The
715-EXCLUDEFILE option might help to inhibit these.
716-
717-Other figleaf problems:
718-
719- the annotated code files are written to BASENAME(file).html, which results
720- in collisions between similarly-named source files.
721-
722- The line-wise coverage information isn't quite right. Blank lines are
723- counted as unreached code, lambdas aren't quite right, and some multiline
724- comments (docstrings?) aren't quite right.
725-
726-"""
727-
728-from twisted.trial.reporter import TreeReporter, VerboseTextReporter
729-
730-# These plugins are registered via twisted/plugins/allmydata_trial.py . See
731-# the notes there for an explanation of how that works.
732-
733-
734-
735-# Reporters don't really get told about the suite starting and stopping.
736-
737-# The Reporter class is imported before the test classes are.
738-
739-# The test classes are imported before the Reporter is created. To get
740-# control earlier than that requires modifying twisted/scripts/trial.py .
741-
742-# Then Reporter.__init__ is called.
743-
744-# Then tests run, calling things like write() and addSuccess(). Each test is
745-# framed by a startTest/stopTest call.
746-
747-# Then the results are emitted, calling things like printErrors,
748-# printSummary, and wasSuccessful.
749-
750-# So for code-coverage (not including import), start in __init__ and finish
751-# in printSummary. To include import, we have to start in our own import and
752-# finish in printSummary.
753-
754-import figleaf
755-figleaf.start()
756-
757-
758-class FigleafReporter(TreeReporter):
759-    def __init__(self, *args, **kwargs):
760-        TreeReporter.__init__(self, *args, **kwargs)
761-
762-    def stop_figleaf(self):
763-        figleaf.stop()
764-        figleaf.write_coverage(".figleaf")
765-        print "Figleaf results written to .figleaf"
766-    def printSummary(self):
767-        # for twisted-2.5.x
768-        self.stop_figleaf()
769-        return TreeReporter.printSummary(self)
770-    def done(self):
771-        # for twisted-8.x
772-        self.stop_figleaf()
773-        return TreeReporter.done(self)
774-
775-class FigleafTextReporter(VerboseTextReporter):
776-    def __init__(self, *args, **kwargs):
777-        VerboseTextReporter.__init__(self, *args, **kwargs)
778-
779-    def stop_figleaf(self):
780-        figleaf.stop()
781-        figleaf.write_coverage(".figleaf")
782-        print "Figleaf results written to .figleaf"
783-    def printSummary(self):
784-        # for twisted-2.5.x
785-        self.stop_figleaf()
786-        return VerboseTextReporter.printSummary(self)
787-    def done(self):
788-        # for twisted-8.x
789-        self.stop_figleaf()
790-        return VerboseTextReporter.done(self)
791-
792-class not_FigleafReporter(object):
793-    # this class, used as a reporter on a fully-passing test suite, doesn't
794-    # trigger exceptions. So it is a guide to what methods are invoked on a
795-    # Reporter.
796-    def __init__(self, *args, **kwargs):
797-        print "FIGLEAF HERE"
798-        self.r = TreeReporter(*args, **kwargs)
799-        self.shouldStop = self.r.shouldStop
800-        self.separator = self.r.separator
801-        self.testsRun = self.r.testsRun
802-        self._starting2 = False
803-
804-    def write(self, *args):
805-        if not self._starting2:
806-            self._starting2 = True
807-            print "FIRST WRITE"
808-        return self.r.write(*args)
809-
810-    def startTest(self, *args, **kwargs):
811-        return self.r.startTest(*args, **kwargs)
812-
813-    def stopTest(self, *args, **kwargs):
814-        return self.r.stopTest(*args, **kwargs)
815-
816-    def addSuccess(self, *args, **kwargs):
817-        return self.r.addSuccess(*args, **kwargs)
818-
819-    def printErrors(self, *args, **kwargs):
820-        return self.r.printErrors(*args, **kwargs)
821-
822-    def writeln(self, *args, **kwargs):
823-        return self.r.writeln(*args, **kwargs)
824-
825-    def printSummary(self, *args, **kwargs):
826-        print "PRINT SUMMARY"
827-        return self.r.printSummary(*args, **kwargs)
828-
829-    def wasSuccessful(self, *args, **kwargs):
830-        return self.r.wasSuccessful(*args, **kwargs)
831-
832diff --git a/twisted/plugins/allmydata_trial.py b/twisted/plugins/allmydata_trial.py
833index 275bbb2..11cbead 100644
834--- a/twisted/plugins/allmydata_trial.py
835+++ b/twisted/plugins/allmydata_trial.py
836@@ -4,18 +4,18 @@ from zope.interface import implements
837 from twisted.trial.itrial import IReporter
838 from twisted.plugin import IPlugin
839 
840-# register a plugin that can create our FigleafReporter. The reporter itself
841-# lives in a separate place
842-
843-# note that this .py file is *not* in a package: there is no __init__.py in
844-# our parent directory. This is important, because otherwise ours would fight
845-# with Twisted's. When trial looks for plugins, it merely executes all the
846-# *.py files it finds in any twisted/plugins/ subdirectories of anything on
847-# sys.path . The namespace that results from executing these .py files is
848-# examined for instances which provide both IPlugin and the target interface
849-# (in this case, trial is looking for IReporter instances). Each such
850-# instance tells the application how to create a plugin by naming the module
851-# and class that should be instantiated.
852+# register a plugin that can create our CoverageReporter. The reporter itself
853+# lives separately, in src/allmydata/test/trial_figleaf.py
854+
855+# note that this allmydata_trial.py file is *not* in a package: there is no
856+# __init__.py in our parent directory. This is important, because otherwise
857+# ours would fight with Twisted's. When trial looks for plugins, it merely
858+# executes all the *.py files it finds in any twisted/plugins/ subdirectories
859+# of anything on sys.path . The namespace that results from executing these
860+# .py files is examined for instances which provide both IPlugin and the
861+# target interface (in this case, trial is looking for IReporter instances).
862+# Each such instance tells the application how to create a plugin by naming
863+# the module and class that should be instantiated.
864 
865 # When installing our package via setup.py, arrange for this file to be
866 # installed to the system-wide twisted/plugins/ directory.
867@@ -32,17 +32,10 @@ class _Reporter(object):
868         self.klass = klass
869 
870 
871-fig = _Reporter("Figleaf Code-Coverage Reporter",
872-                "allmydata.test.trial_figleaf",
873-                description="verbose color output (with figleaf coverage)",
874-                longOpt="verbose-figleaf",
875-                shortOpt="f",
876-                klass="FigleafReporter")
877-
878-bwfig = _Reporter("Figleaf Code-Coverage Reporter (colorless)",
879-                  "allmydata.test.trial_figleaf",
880-                  description="Colorless verbose output (with figleaf coverage)",
881-                  longOpt="bwverbose-figleaf",
882+bwcov = _Reporter("Code-Coverage Reporter (colorless)",
883+                  "allmydata.test.trial_coverage",
884+                  description="Colorless verbose output (with 'coverage' coverage)",
885+                  longOpt="bwverbose-coverage",
886                   shortOpt=None,
887-                  klass="FigleafTextReporter")
888+                  klass="CoverageTextReporter")
889