Merge lp://staging/~mbp/subunit/408821-filter-re into lp://staging/~subunit/subunit/trunk
- 408821-filter-re
- Merge into trunk
Status: | Superseded | ||||
---|---|---|---|---|---|
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: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Subunit Developers | Pending | ||
Review via email: mp+9680@code.staging.launchpad.net |
This proposal has been superseded by a proposal from 2009-08-06.
Commit message
Description of the change
Martin Pool (mbp) wrote : | # |
Martin Pool (mbp) wrote : | # |
> I anticipate it being very
> useful.
yep, it is :)
Robert Collins (lifeless) wrote : | # |
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 : | # |
> 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.
- 79. By Martin Pool
-
Clean up and rename subunit-filter --with
- 80. By Martin Pool
-
Help message for --with/without
- 81. By Martin Pool
-
Build regexp filter as a closure not using globals
Unmerged revisions
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-05 07:21:05 +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 | @@ -25,6 +26,7 @@ |
45 | from optparse import OptionParser |
46 | import sys |
47 | import unittest |
48 | +import re |
49 | |
50 | from subunit import ( |
51 | DiscardStream, |
52 | @@ -50,11 +52,37 @@ |
53 | help="exclude skips", dest="skip") |
54 | parser.add_option("--no-success", action="store_true", |
55 | help="exclude successes", default=True, dest="success") |
56 | +parser.add_option("-m", "--match", type=str, |
57 | + help="regexp to include (case-sensitive by default)") |
58 | +parser.add_option("--anti-match", type=str, |
59 | + help="regexp to exclude (case-sensitive by default)") |
60 | + |
61 | (options, args) = parser.parse_args() |
62 | + |
63 | +def make_re_matcher(needle_re_string): |
64 | + needle_re = re.compile(needle_re_string, re.MULTILINE) |
65 | + return lambda test, err: ( |
66 | + needle_re.search(str(test)) or |
67 | + ((err is not None) and needle_re.search(str(err)))) |
68 | + |
69 | +if options.match: |
70 | + match_predicate = make_re_matcher(options.match) |
71 | +else: |
72 | + match_predicate = lambda test, err: True |
73 | + |
74 | +if options.anti_match: |
75 | + anti_match_predicate = make_re_matcher(options.anti_match) |
76 | +else: |
77 | + anti_match_predicate = lambda test, err: False |
78 | + |
79 | +filter_predicate = lambda test, err: ( |
80 | + match_predicate(test, err) and not anti_match_predicate(test, err)) |
81 | + |
82 | result = TestProtocolClient(sys.stdout) |
83 | result = TestResultFilter(result, filter_error=options.error, |
84 | filter_failure=options.failure, filter_success=options.success, |
85 | - filter_skip=options.skip) |
86 | + filter_skip=options.skip, |
87 | + filter_predicate=filter_predicate) |
88 | if options.no_passthrough: |
89 | passthrough_stream = DiscardStream() |
90 | else: |
91 | |
92 | === modified file 'python/subunit/__init__.py' |
93 | --- python/subunit/__init__.py 2009-08-02 22:55:36 +0000 |
94 | +++ python/subunit/__init__.py 2009-08-05 06:57:48 +0000 |
95 | @@ -792,16 +792,22 @@ |
96 | the other instance must be interrogated. |
97 | |
98 | :ivar result: The result that tests are passed to after filtering. |
99 | + :ivar filter_predicate: The callback run to decide whether to pass |
100 | + a result. |
101 | """ |
102 | |
103 | def __init__(self, result, filter_error=False, filter_failure=False, |
104 | - filter_success=True, filter_skip=False): |
105 | + filter_success=True, filter_skip=False, |
106 | + filter_predicate=None): |
107 | """Create a FilterResult object filtering to result. |
108 | |
109 | :param filter_error: Filter out errors. |
110 | :param filter_failure: Filter out failures. |
111 | :param filter_success: Filter out successful tests. |
112 | :param filter_skip: Filter out skipped tests. |
113 | + :param filter_predicate: A callable taking (test, err) and |
114 | + returning True if the result should be passed through. |
115 | + err is None for success. |
116 | """ |
117 | unittest.TestResult.__init__(self) |
118 | self.result = result |
119 | @@ -809,21 +815,24 @@ |
120 | self._filter_failure = filter_failure |
121 | self._filter_success = filter_success |
122 | self._filter_skip = filter_skip |
123 | + if filter_predicate is None: |
124 | + filter_predicate = lambda test, err: True |
125 | + self.filter_predicate = filter_predicate |
126 | |
127 | def addError(self, test, err): |
128 | - if not self._filter_error: |
129 | + if not self._filter_error and self.filter_predicate(test, err): |
130 | self.result.startTest(test) |
131 | self.result.addError(test, err) |
132 | self.result.stopTest(test) |
133 | |
134 | def addFailure(self, test, err): |
135 | - if not self._filter_failure: |
136 | + if not self._filter_failure and self.filter_predicate(test, err): |
137 | self.result.startTest(test) |
138 | self.result.addFailure(test, err) |
139 | self.result.stopTest(test) |
140 | |
141 | def addSkip(self, test, reason): |
142 | - if not self._filter_skip: |
143 | + if not self._filter_skip and self.filter_predicate(test, reason): |
144 | self.result.startTest(test) |
145 | # This is duplicated, it would be nice to have on a 'calls |
146 | # TestResults' mixin perhaps. |
147 | @@ -835,7 +844,7 @@ |
148 | self.result.stopTest(test) |
149 | |
150 | def addSuccess(self, test): |
151 | - if not self._filter_success: |
152 | + if not self._filter_success and self.filter_predicate(test, None): |
153 | self.result.startTest(test) |
154 | self.result.addSuccess(test) |
155 | self.result.stopTest(test) |
156 | |
157 | === modified file 'python/subunit/tests/test_subunit_filter.py' |
158 | --- python/subunit/tests/test_subunit_filter.py 2009-03-08 20:34:19 +0000 |
159 | +++ python/subunit/tests/test_subunit_filter.py 2009-08-05 06:57:48 +0000 |
160 | @@ -91,6 +91,19 @@ |
161 | self.filtered_result.failures]) |
162 | self.assertEqual(5, self.filtered_result.testsRun) |
163 | |
164 | + def test_filter_predicate(self): |
165 | + """You can filter by predicate callbacks""" |
166 | + self.filtered_result = unittest.TestResult() |
167 | + filter_cb = lambda test, err: str(err).find('error details') != -1 |
168 | + self.filter = subunit.TestResultFilter(self.filtered_result, |
169 | + filter_predicate=filter_cb, |
170 | + filter_success=False) |
171 | + self.run_tests() |
172 | + self.assertEqual(1, |
173 | + self.filtered_result.testsRun) |
174 | + # I'd like to test filtering the xfail but it's blocked by |
175 | + # https://bugs.edge.launchpad.net/subunit/+bug/409193 -- mbp 20090805 |
176 | + |
177 | def run_tests(self): |
178 | self.setUpTestStream() |
179 | self.test = subunit.ProtocolTestCase(self.input_stream) |
180 | @@ -109,7 +122,9 @@ |
181 | tags: local |
182 | failure failed |
183 | test error |
184 | -error error |
185 | +error error [ |
186 | +error details |
187 | +] |
188 | test skipped |
189 | skip skipped |
190 | test todo |
Tested interactively, not yet tested anger, but I anticipate it being very useful.