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

Proposed by Martin Pool
Status: Superseded
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
Reviewer Review Type Date Requested Status
Subunit Developers Pending
Review via email: mp+9680@code.launchpad.net

This proposal has been superseded by a proposal from 2009-08-06.

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

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

Revision history for this message
Martin Pool (mbp) wrote :

> I anticipate it being very
> useful.

yep, it is :)

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

> 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.

lp:~mbp/subunit/408821-filter-re updated
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

[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-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

Subscribers

People subscribed via source and target branches