Merge lp:~allenap/maas/tests-emit-subunit into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5616
Proposed branch: lp:~allenap/maas/tests-emit-subunit
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 190 lines (+112/-3)
3 files modified
HACKING.txt (+13/-0)
src/maastesting/noseplug.py (+48/-1)
src/maastesting/tests/test_noseplug.py (+51/-2)
To merge this branch: bzr merge lp:~allenap/maas/tests-emit-subunit
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+313201@code.launchpad.net

Commit message

Make it possible to emit subunit from test runs.

Description of the change

The nose-subunit plugin at https://github.com/liucougar/nose-subunit seems to have bit-rotted and does not work for me. This is a very simple replacement that seems to work fine, but may need augmenting to cooperate with other plugins, but we can deal with those problems as we encounter them.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I want to approve this but I have no idea what it does and how to use it. ;-)

Can you explain more about what this branch is all about? Maybe add something to HACKING if appropriate?

review: Needs Information
Revision history for this message
Gavin Panella (allenap) wrote :

This adds a --with-subunit flag to nose. Subunit is an on-the-wire format for streaming test results. This stream can be used (with the subunit library) to manipulate a TestResult object elsewhere. In other words, it could be used to run a test in another process (perhaps on another machine) but capture results in nose locally without a significant loss of fidelity. This is part of an effort to run the test suite in parallel. This branch adds only the emission of subunit, optionally to another fd using --subunit-fd, so we can be sure of a clean stream.

I've added a paragraph to HACKING.txt.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Very nice! Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING.txt'
2--- HACKING.txt 2016-12-07 12:46:14 +0000
3+++ HACKING.txt 2016-12-20 07:56:25 +0000
4@@ -108,6 +108,19 @@
5 changing your locales on your workstation to ones present on the
6 server will solve the issue.
7
8+
9+Emitting subunit
10+^^^^^^^^^^^^^^^^
11+
12+Pass the ``--with-subunit`` flag to any of the test runners (e.g.
13+``bin/test.rack``) to produce a `subunit`_ stream of test results. This
14+may be useful for parallelising test runs, or to allow later analysis of
15+a test run. The optional ``--subunit-fd`` flag can be used to direct the
16+results to a different file descriptor, to ensure a clean stream.
17+
18+.. _subunit: https://launchpad.net/subunit/
19+
20+
21 Running JavaScript tests
22 ^^^^^^^^^^^^^^^^^^^^^^^^
23
24
25=== modified file 'src/maastesting/noseplug.py'
26--- src/maastesting/noseplug.py 2016-07-07 11:59:07 +0000
27+++ src/maastesting/noseplug.py 2016-12-20 07:56:25 +0000
28@@ -12,6 +12,7 @@
29 ]
30
31 import inspect
32+import io
33 import logging
34 import unittest
35
36@@ -261,6 +262,52 @@
37 return inspect.getdoc(self)
38
39
40+class Subunit(Plugin):
41+ """Emit test results as a subunit stream."""
42+
43+ name = "subunit"
44+ option_fd = "%s_fd" % name
45+ log = logging.getLogger('nose.plugins.%s' % name)
46+ score = 2000 # Run really early, beating even xunit.
47+
48+ def options(self, parser, env):
49+ """Add options to Nose's parser.
50+
51+ :attention: This is part of the Nose plugin contract.
52+ """
53+ super(Subunit, self).options(parser, env)
54+ parser.add_option(
55+ "--%s-fd" % self.name, type=int,
56+ dest=self.option_fd, action="store", default=1, help=(
57+ "Emit subunit via a specific numeric file descriptor, "
58+ "stdout (1) by default."
59+ ),
60+ metavar="FD",
61+ )
62+
63+ def configure(self, options, conf):
64+ """Configure, based on the parsed options.
65+
66+ :attention: This is part of the Nose plugin contract.
67+ """
68+ super(Subunit, self).configure(options, conf)
69+ if self.enabled:
70+ # Process --${name}-fd.
71+ fd = getattr(options, self.option_fd)
72+ self.stream = io.open(fd, "wb")
73+
74+ def prepareTestResult(self, result):
75+ from subunit import TestProtocolClient
76+ return TestProtocolClient(self.stream)
77+
78+ def help(self):
79+ """Used in the --help text.
80+
81+ :attention: This is part of the Nose plugin contract.
82+ """
83+ return inspect.getdoc(self)
84+
85+
86 def main():
87 """Invoke Nose's `TestProgram` with extra plugins.
88
89@@ -269,5 +316,5 @@
90 flags ``--with-crochet``, ``--with-resources``, ``--with-scenarios``,
91 and/or ``--with-select``.
92 """
93- plugins = Crochet(), Resources(), Scenarios(), Select()
94+ plugins = Crochet(), Resources(), Scenarios(), Select(), Subunit()
95 return TestProgram(addplugins=plugins)
96
97=== modified file 'src/maastesting/tests/test_noseplug.py'
98--- src/maastesting/tests/test_noseplug.py 2016-07-07 11:59:07 +0000
99+++ src/maastesting/tests/test_noseplug.py 2016-12-20 07:56:25 +0000
100@@ -6,7 +6,10 @@
101 __all__ = []
102
103 from optparse import OptionParser
104-from os import makedirs
105+from os import (
106+ devnull,
107+ makedirs,
108+)
109 from os.path import (
110 dirname,
111 join,
112@@ -30,14 +33,17 @@
113 Resources,
114 Scenarios,
115 Select,
116+ Subunit,
117 )
118 from maastesting.testcase import MAASTestCase
119 import nose.case
120+from subunit import TestProtocolClient
121 from testresources import OptimisingTestSuite
122 from testtools.matchers import (
123 AllMatch,
124 Equals,
125 HasLength,
126+ Is,
127 IsInstance,
128 MatchesListwise,
129 MatchesSetwise,
130@@ -356,6 +362,48 @@
131 join(directory, factory.make_name("other-child"))))
132
133
134+class TestSubunit(MAASTestCase):
135+
136+ def test__options_adds_options(self):
137+ select = Subunit()
138+ parser = OptionParser()
139+ select.options(parser=parser, env={})
140+ self.assertThat(
141+ parser.option_list[-2:],
142+ MatchesListwise([
143+ # The --with-subunit option.
144+ MatchesStructure.byEquality(
145+ action="store_true", default=None,
146+ dest="enable_plugin_subunit",
147+ ),
148+ # The --subunit-fd option.
149+ MatchesStructure.byEquality(
150+ action="store", default=1, dest="subunit_fd",
151+ metavar="FD", type="int", _short_opts=[],
152+ _long_opts=["--subunit-fd"],
153+ )
154+ ]))
155+
156+ def test__configure_opens_stream(self):
157+ subunit = Subunit()
158+ parser = OptionParser()
159+ subunit.add_options(parser=parser, env={})
160+ with open(devnull, "wb") as fd:
161+ options, rest = parser.parse_args(
162+ ["--with-subunit", "--subunit-fd", str(fd.fileno())])
163+ subunit.configure(options, sentinel.conf)
164+ self.assertThat(subunit.stream.fileno(), Equals(fd.fileno()))
165+ self.assertThat(subunit.stream.mode, Equals("wb"))
166+
167+ def test__prepareTestResult_returns_subunit_client(self):
168+ subunit = Subunit()
169+ with open(devnull, "wb") as stream:
170+ subunit.stream = stream
171+ result = subunit.prepareTestResult(sentinel.result)
172+ self.assertThat(result, IsInstance(TestProtocolClient))
173+ self.assertThat(result, MatchesStructure(_stream=Is(stream)))
174+
175+
176 class TestMain(MAASTestCase):
177
178 def test__sets_addplugins(self):
179@@ -363,9 +411,10 @@
180 noseplug.main()
181 self.assertThat(
182 noseplug.TestProgram,
183- MockCalledOnceWith(addplugins=(ANY, ANY, ANY, ANY)))
184+ MockCalledOnceWith(addplugins=(ANY, ANY, ANY, ANY, ANY)))
185 plugins = noseplug.TestProgram.call_args[1]["addplugins"]
186 self.assertThat(plugins, MatchesSetwise(
187 IsInstance(Crochet), IsInstance(Resources),
188 IsInstance(Scenarios), IsInstance(Select),
189+ IsInstance(Subunit),
190 ))