Merge lp://staging/~mbp/subunit/408821-filter-re into lp://staging/~subunit/subunit/trunk
- 408821-filter-re
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | not available | ||||
Proposed branch: | lp://staging/~mbp/subunit/408821-filter-re | ||||
Merge into: | lp://staging/~subunit/subunit/trunk | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp://staging/~mbp/subunit/408821-filter-re | ||||
Related bugs: |
|
This proposal supersedes a proposal from 2009-08-05.
Commit message
Description of the change
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
> I anticipate it being very
> useful.
yep, it is :)
Robert Collins (lifeless) wrote : Posted in a previous version of this proposal | # |
A few thoughts. Broadly this is fine and I like the feature.
Rather than document in README, perhaps extending the help (module
docstring) of subunit-filter would be more accessible and provide a home
for other such one-command things. OTOH we probably need a manual at
some point anyway, so up to you.
Rather than --[anti]-match, I'd rather something like:
--with RE
--without RE
Related to that, I wonder if without is really needed - the (?!...) can
embed negative patterns. I'm fine with it, just speculating.
In the internals though, I think it would make sense to have a single
re:
patterns = []
for pattern in options.
patterns.
for pattern in options.
patterns.
rule = "|".join(patterns)
The interface is a little awkward - you use a functional style, but it
doesn't fit all that well. For instance - "str(None) -> 'None'". So,
your code will incorrectly match on a pattern like 'one'.than
I think I'd prefer it if, in the Filter result you did:
should_filter = self._check_
and that function (not a lambda) does
if not callable(
return False
else:
if info is None:
info = ''
else:
info = str(info)
return self._filter_
And that way folk writing predicates can be sure they are dealing with
strings - we centralise that knowledge.
Lastly, and this can come later if you like, I'd really like the ability
to specify multiple patterns, which optparse can do quite easily with
store="append".
-Rob
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
> A few thoughts. Broadly this is fine and I like the feature.
>
> Rather than document in README, perhaps extending the help (module
> docstring) of subunit-filter would be more accessible and provide a home
> for other such one-command things. OTOH we probably need a manual at
> some point anyway, so up to you.
I think you need some overall "here's what you can do with it" documentation, so I put it there.
>
> Rather than --[anti]-match, I'd rather something like:
> --with RE
> --without RE
ok
>
> Related to that, I wonder if without is really needed - the (?!...) can
> embed negative patterns. I'm fine with it, just speculating.
jeff's mum will never guess that.
> In the internals though, I think it would make sense to have a single
> re:
> patterns = []
> for pattern in options.
> patterns.
> for pattern in options.
> patterns.
> rule = "|".join(patterns)
I think it's much more useful to have (A AND NOT B) than (A OR NOT B) as far as cutting down what's present in the output.
> The interface is a little awkward - you use a functional style, but it
> doesn't fit all that well. For instance - "str(None) -> 'None'".
That seems like a non sequiter.
And I guess I see subunit as generally wanting to be functional.
> So,
> your code will incorrectly match on a pattern like 'one'.than
I don't think so, unless the test can be None.
>
> I think I'd prefer it if, in the Filter result you did:
> should_filter = self._check_
>
> and that function (not a lambda) does
> if not callable(
> return False
> else:
> if info is None:
> info = ''
> else:
> info = str(info)
> return self._filter_
1- Python sucks :-/, if I wanted 'if (a!=NULL)' everywhere I'd use C :-}
2- I think it's useful to allow future code to provide filters that look at objects not strings, seeing as the rest of it deals with objects. I realize they will mostly be lame objects that just wrap strings, so perhaps it's not worth it. If I was looking at bzr errors in-process I might want to poke through their attributes. For instance you might want to pass isinstance(err, KnownFailure).
3- Doing nothing if an object is not the type you expect is pretty error prone.
4- Looking at it now I'd like to fold *all* the filtering into evaluation of a predicate, so that this code was just changing one method not ~five. But that's a larger change and ties to your thing of how the status should be handled.
Sorry if this sounds grumpy. Thanks for the fast review.
> And that way folk writing predicates can be sure they are dealing with
> strings - we centralise that knowledge.
>
> Lastly, and this can come later if you like, I'd really like the ability
> to specify multiple patterns, which optparse can do quite easily with
> store="append".
Agree, but later.
Martin Pool (mbp) wrote : | # |
I haven't done everything we talked about but this possibly makes the style more Pythonic - let me know.
Still to do:
* rearrange TestResultFilter to do this separately from filtering on kind
* maybe let the callback see the type of result
* fold together everything into addResult
* maybe put _make_regexp_filter into the library and test it
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2009-08-02 22:55:36 +0000 |
3 | +++ NEWS 2009-08-05 06:57:48 +0000 |
4 | @@ -30,6 +30,9 @@ |
5 | stream to a single JUnit style XML stream using the pyjunitxml |
6 | python library. |
7 | |
8 | + * ``TestResultFilter`` takes a new optional constructor parameter |
9 | + ``filter_predicate``. (Martin Pool) |
10 | + |
11 | BUG FIXES: |
12 | |
13 | API CHANGES: |
14 | |
15 | === modified file 'README' |
16 | --- README 2009-08-01 08:01:27 +0000 |
17 | +++ README 2009-08-05 07:22:36 +0000 |
18 | @@ -169,6 +169,13 @@ |
19 | elements, and a patch for adding subunit output to the 'ShUnit' shell test |
20 | runner. See 'shell/README' for details. |
21 | |
22 | +Filter recipes |
23 | +-------------- |
24 | + |
25 | +To ignore some failing tests whose root cause is already known:: |
26 | + |
27 | + subunit-filter --anti-match 'AttributeError.*flavor' |
28 | + |
29 | |
30 | The protocol |
31 | ------------ |
32 | |
33 | === modified file 'filters/subunit-filter' |
34 | --- filters/subunit-filter 2009-08-05 05:21:50 +0000 |
35 | +++ filters/subunit-filter 2009-08-06 04:40:07 +0000 |
36 | @@ -1,6 +1,7 @@ |
37 | #!/usr/bin/env python |
38 | # subunit: extensions to python unittest to get test results from subprocesses. |
39 | # Copyright (C) 2008 Robert Collins <robertc@robertcollins.net> |
40 | +# (C) 2009 Martin Pool |
41 | # |
42 | # This program is free software; you can redistribute it and/or modify |
43 | # it under the terms of the GNU General Public License as published by |
44 | @@ -20,11 +21,18 @@ |
45 | """Filter a subunit stream to include/exclude tests. |
46 | |
47 | The default is to strip successful tests. |
48 | + |
49 | +Tests can be filtered by Python regular expressions with --with and --without, |
50 | +which match both the test name and the error text (if any). The result |
51 | +contains tests which match any of the --with expressions and none of the |
52 | +--without expressions. For case-insensitive matching prepend '(?i)'. |
53 | +Remember to quote shell metacharacters. |
54 | """ |
55 | |
56 | from optparse import OptionParser |
57 | import sys |
58 | import unittest |
59 | +import re |
60 | |
61 | from subunit import ( |
62 | DiscardStream, |
63 | @@ -50,11 +58,51 @@ |
64 | help="exclude skips", dest="skip") |
65 | parser.add_option("--no-success", action="store_true", |
66 | help="exclude successes", default=True, dest="success") |
67 | +parser.add_option("-m", "--with", type=str, |
68 | + help="regexp to include (case-sensitive by default)", |
69 | + action="append", dest="with_regexps") |
70 | +parser.add_option("--without", type=str, |
71 | + help="regexp to exclude (case-sensitive by default)", |
72 | + action="append", dest="without_regexps") |
73 | + |
74 | (options, args) = parser.parse_args() |
75 | + |
76 | + |
77 | +def _compile_re_from_list(l): |
78 | + return re.compile("|".join(l), re.MULTILINE) |
79 | + |
80 | + |
81 | +def _make_regexp_filter(with_regexps, without_regexps): |
82 | + """Make a callback that checks tests against regexps. |
83 | + |
84 | + with_regexps and without_regexps are each either a list of regexp strings, |
85 | + or None. |
86 | + """ |
87 | + with_re = with_regexps and _compile_re_from_list(with_regexps) |
88 | + without_re = without_regexps and _compile_re_from_list(without_regexps) |
89 | + |
90 | + def check_regexps(test, err): |
91 | + """Check if this test and error match the regexp filters.""" |
92 | + test_str = str(test) |
93 | + err_str = err and str(err) # may be None |
94 | + if with_re: |
95 | + if not (with_re.search(test_str) |
96 | + or with_re.search(err_str)): |
97 | + return False |
98 | + if without_re: |
99 | + if (without_re.search(test_str) or without_re.search(err_str)): |
100 | + return False |
101 | + return True |
102 | + return check_regexps |
103 | + |
104 | + |
105 | +regexp_filter = _make_regexp_filter(options.with_regexps, |
106 | + options.without_regexps) |
107 | result = TestProtocolClient(sys.stdout) |
108 | result = TestResultFilter(result, filter_error=options.error, |
109 | filter_failure=options.failure, filter_success=options.success, |
110 | - filter_skip=options.skip) |
111 | + filter_skip=options.skip, |
112 | + filter_predicate=regexp_filter) |
113 | if options.no_passthrough: |
114 | passthrough_stream = DiscardStream() |
115 | else: |
116 | |
117 | === modified file 'python/subunit/__init__.py' |
118 | --- python/subunit/__init__.py 2009-08-02 22:55:36 +0000 |
119 | +++ python/subunit/__init__.py 2009-08-05 06:57:48 +0000 |
120 | @@ -792,16 +792,22 @@ |
121 | the other instance must be interrogated. |
122 | |
123 | :ivar result: The result that tests are passed to after filtering. |
124 | + :ivar filter_predicate: The callback run to decide whether to pass |
125 | + a result. |
126 | """ |
127 | |
128 | def __init__(self, result, filter_error=False, filter_failure=False, |
129 | - filter_success=True, filter_skip=False): |
130 | + filter_success=True, filter_skip=False, |
131 | + filter_predicate=None): |
132 | """Create a FilterResult object filtering to result. |
133 | |
134 | :param filter_error: Filter out errors. |
135 | :param filter_failure: Filter out failures. |
136 | :param filter_success: Filter out successful tests. |
137 | :param filter_skip: Filter out skipped tests. |
138 | + :param filter_predicate: A callable taking (test, err) and |
139 | + returning True if the result should be passed through. |
140 | + err is None for success. |
141 | """ |
142 | unittest.TestResult.__init__(self) |
143 | self.result = result |
144 | @@ -809,21 +815,24 @@ |
145 | self._filter_failure = filter_failure |
146 | self._filter_success = filter_success |
147 | self._filter_skip = filter_skip |
148 | + if filter_predicate is None: |
149 | + filter_predicate = lambda test, err: True |
150 | + self.filter_predicate = filter_predicate |
151 | |
152 | def addError(self, test, err): |
153 | - if not self._filter_error: |
154 | + if not self._filter_error and self.filter_predicate(test, err): |
155 | self.result.startTest(test) |
156 | self.result.addError(test, err) |
157 | self.result.stopTest(test) |
158 | |
159 | def addFailure(self, test, err): |
160 | - if not self._filter_failure: |
161 | + if not self._filter_failure and self.filter_predicate(test, err): |
162 | self.result.startTest(test) |
163 | self.result.addFailure(test, err) |
164 | self.result.stopTest(test) |
165 | |
166 | def addSkip(self, test, reason): |
167 | - if not self._filter_skip: |
168 | + if not self._filter_skip and self.filter_predicate(test, reason): |
169 | self.result.startTest(test) |
170 | # This is duplicated, it would be nice to have on a 'calls |
171 | # TestResults' mixin perhaps. |
172 | @@ -835,7 +844,7 @@ |
173 | self.result.stopTest(test) |
174 | |
175 | def addSuccess(self, test): |
176 | - if not self._filter_success: |
177 | + if not self._filter_success and self.filter_predicate(test, None): |
178 | self.result.startTest(test) |
179 | self.result.addSuccess(test) |
180 | self.result.stopTest(test) |
181 | |
182 | === modified file 'python/subunit/tests/test_subunit_filter.py' |
183 | --- python/subunit/tests/test_subunit_filter.py 2009-03-08 20:34:19 +0000 |
184 | +++ python/subunit/tests/test_subunit_filter.py 2009-08-05 06:57:48 +0000 |
185 | @@ -91,6 +91,19 @@ |
186 | self.filtered_result.failures]) |
187 | self.assertEqual(5, self.filtered_result.testsRun) |
188 | |
189 | + def test_filter_predicate(self): |
190 | + """You can filter by predicate callbacks""" |
191 | + self.filtered_result = unittest.TestResult() |
192 | + filter_cb = lambda test, err: str(err).find('error details') != -1 |
193 | + self.filter = subunit.TestResultFilter(self.filtered_result, |
194 | + filter_predicate=filter_cb, |
195 | + filter_success=False) |
196 | + self.run_tests() |
197 | + self.assertEqual(1, |
198 | + self.filtered_result.testsRun) |
199 | + # I'd like to test filtering the xfail but it's blocked by |
200 | + # https://bugs.edge.launchpad.net/subunit/+bug/409193 -- mbp 20090805 |
201 | + |
202 | def run_tests(self): |
203 | self.setUpTestStream() |
204 | self.test = subunit.ProtocolTestCase(self.input_stream) |
205 | @@ -109,7 +122,9 @@ |
206 | tags: local |
207 | failure failed |
208 | test error |
209 | -error error |
210 | +error error [ |
211 | +error details |
212 | +] |
213 | test skipped |
214 | skip skipped |
215 | test todo |
Tested interactively, not yet tested anger, but I anticipate it being very useful.