Merge lp:~mbp/subunit/408821-filter-re into lp:~subunit/subunit/trunk

Proposed by Martin Pool
Status: Merged
Merged at revision: not available
Proposed branch: lp:~mbp/subunit/408821-filter-re
Merge into: lp:~subunit/subunit/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~mbp/subunit/408821-filter-re

This proposal supersedes a proposal from 2009-08-05.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

Tested interactively, not yet tested anger, but I anticipate it being very useful.

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> I anticipate it being very
> useful.

yep, it is :)

Revision history for this message
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.with_patterns:
  patterns.append(pattern)
for pattern in options.without_patterns:
  patterns.append("^(?!%s)$" % pattern)
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_predicates(test, reason|error)

and that function (not a lambda) does
if not callable(self._filter_predicate):
    return False
else:
    if info is None:
        info = ''
    else:
        info = str(info)
    return self._filter_predicate(test, info)

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

Revision history for this message
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.with_patterns:
> patterns.append(pattern)
> for pattern in options.without_patterns:
> patterns.append("^(?!%s)$" % pattern)
> 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_predicates(test, reason|error)
>
> and that function (not a lambda) does
> if not callable(self._filter_predicate):
> return False
> else:
> if info is None:
> info = ''
> else:
> info = str(info)
> return self._filter_predicate(test, info)

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.

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches