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
=== modified file 'NEWS'
--- NEWS 2009-08-02 22:55:36 +0000
+++ NEWS 2009-08-05 06:57:48 +0000
@@ -30,6 +30,9 @@
30 stream to a single JUnit style XML stream using the pyjunitxml30 stream to a single JUnit style XML stream using the pyjunitxml
31 python library.31 python library.
3232
33 * ``TestResultFilter`` takes a new optional constructor parameter
34 ``filter_predicate``. (Martin Pool)
35
33 BUG FIXES:36 BUG FIXES:
3437
35 API CHANGES:38 API CHANGES:
3639
=== modified file 'README'
--- README 2009-08-01 08:01:27 +0000
+++ README 2009-08-05 07:22:36 +0000
@@ -169,6 +169,13 @@
169elements, and a patch for adding subunit output to the 'ShUnit' shell test169elements, and a patch for adding subunit output to the 'ShUnit' shell test
170runner. See 'shell/README' for details.170runner. See 'shell/README' for details.
171171
172Filter recipes
173--------------
174
175To ignore some failing tests whose root cause is already known::
176
177 subunit-filter --anti-match 'AttributeError.*flavor'
178
172179
173The protocol180The protocol
174------------181------------
175182
=== modified file 'filters/subunit-filter'
--- filters/subunit-filter 2009-08-05 05:21:50 +0000
+++ filters/subunit-filter 2009-08-05 07:21:05 +0000
@@ -1,6 +1,7 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# subunit: extensions to python unittest to get test results from subprocesses.2# subunit: extensions to python unittest to get test results from subprocesses.
3# Copyright (C) 2008 Robert Collins <robertc@robertcollins.net>3# Copyright (C) 2008 Robert Collins <robertc@robertcollins.net>
4# (C) 2009 Martin Pool
4#5#
5# This program is free software; you can redistribute it and/or modify6# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -25,6 +26,7 @@
25from optparse import OptionParser26from optparse import OptionParser
26import sys27import sys
27import unittest28import unittest
29import re
2830
29from subunit import (31from subunit import (
30 DiscardStream,32 DiscardStream,
@@ -50,11 +52,37 @@
50 help="exclude skips", dest="skip")52 help="exclude skips", dest="skip")
51parser.add_option("--no-success", action="store_true",53parser.add_option("--no-success", action="store_true",
52 help="exclude successes", default=True, dest="success")54 help="exclude successes", default=True, dest="success")
55parser.add_option("-m", "--match", type=str,
56 help="regexp to include (case-sensitive by default)")
57parser.add_option("--anti-match", type=str,
58 help="regexp to exclude (case-sensitive by default)")
59
53(options, args) = parser.parse_args()60(options, args) = parser.parse_args()
61
62def make_re_matcher(needle_re_string):
63 needle_re = re.compile(needle_re_string, re.MULTILINE)
64 return lambda test, err: (
65 needle_re.search(str(test)) or
66 ((err is not None) and needle_re.search(str(err))))
67
68if options.match:
69 match_predicate = make_re_matcher(options.match)
70else:
71 match_predicate = lambda test, err: True
72
73if options.anti_match:
74 anti_match_predicate = make_re_matcher(options.anti_match)
75else:
76 anti_match_predicate = lambda test, err: False
77
78filter_predicate = lambda test, err: (
79 match_predicate(test, err) and not anti_match_predicate(test, err))
80
54result = TestProtocolClient(sys.stdout)81result = TestProtocolClient(sys.stdout)
55result = TestResultFilter(result, filter_error=options.error,82result = TestResultFilter(result, filter_error=options.error,
56 filter_failure=options.failure, filter_success=options.success,83 filter_failure=options.failure, filter_success=options.success,
57 filter_skip=options.skip)84 filter_skip=options.skip,
85 filter_predicate=filter_predicate)
58if options.no_passthrough:86if options.no_passthrough:
59 passthrough_stream = DiscardStream()87 passthrough_stream = DiscardStream()
60else:88else:
6189
=== modified file 'python/subunit/__init__.py'
--- python/subunit/__init__.py 2009-08-02 22:55:36 +0000
+++ python/subunit/__init__.py 2009-08-05 06:57:48 +0000
@@ -792,16 +792,22 @@
792 the other instance must be interrogated.792 the other instance must be interrogated.
793793
794 :ivar result: The result that tests are passed to after filtering.794 :ivar result: The result that tests are passed to after filtering.
795 :ivar filter_predicate: The callback run to decide whether to pass
796 a result.
795 """797 """
796798
797 def __init__(self, result, filter_error=False, filter_failure=False,799 def __init__(self, result, filter_error=False, filter_failure=False,
798 filter_success=True, filter_skip=False):800 filter_success=True, filter_skip=False,
801 filter_predicate=None):
799 """Create a FilterResult object filtering to result.802 """Create a FilterResult object filtering to result.
800 803
801 :param filter_error: Filter out errors.804 :param filter_error: Filter out errors.
802 :param filter_failure: Filter out failures.805 :param filter_failure: Filter out failures.
803 :param filter_success: Filter out successful tests.806 :param filter_success: Filter out successful tests.
804 :param filter_skip: Filter out skipped tests.807 :param filter_skip: Filter out skipped tests.
808 :param filter_predicate: A callable taking (test, err) and
809 returning True if the result should be passed through.
810 err is None for success.
805 """811 """
806 unittest.TestResult.__init__(self)812 unittest.TestResult.__init__(self)
807 self.result = result813 self.result = result
@@ -809,21 +815,24 @@
809 self._filter_failure = filter_failure815 self._filter_failure = filter_failure
810 self._filter_success = filter_success816 self._filter_success = filter_success
811 self._filter_skip = filter_skip817 self._filter_skip = filter_skip
818 if filter_predicate is None:
819 filter_predicate = lambda test, err: True
820 self.filter_predicate = filter_predicate
812 821
813 def addError(self, test, err):822 def addError(self, test, err):
814 if not self._filter_error:823 if not self._filter_error and self.filter_predicate(test, err):
815 self.result.startTest(test)824 self.result.startTest(test)
816 self.result.addError(test, err)825 self.result.addError(test, err)
817 self.result.stopTest(test)826 self.result.stopTest(test)
818827
819 def addFailure(self, test, err):828 def addFailure(self, test, err):
820 if not self._filter_failure:829 if not self._filter_failure and self.filter_predicate(test, err):
821 self.result.startTest(test)830 self.result.startTest(test)
822 self.result.addFailure(test, err)831 self.result.addFailure(test, err)
823 self.result.stopTest(test)832 self.result.stopTest(test)
824833
825 def addSkip(self, test, reason):834 def addSkip(self, test, reason):
826 if not self._filter_skip:835 if not self._filter_skip and self.filter_predicate(test, reason):
827 self.result.startTest(test)836 self.result.startTest(test)
828 # This is duplicated, it would be nice to have on a 'calls837 # This is duplicated, it would be nice to have on a 'calls
829 # TestResults' mixin perhaps.838 # TestResults' mixin perhaps.
@@ -835,7 +844,7 @@
835 self.result.stopTest(test)844 self.result.stopTest(test)
836845
837 def addSuccess(self, test):846 def addSuccess(self, test):
838 if not self._filter_success:847 if not self._filter_success and self.filter_predicate(test, None):
839 self.result.startTest(test)848 self.result.startTest(test)
840 self.result.addSuccess(test)849 self.result.addSuccess(test)
841 self.result.stopTest(test)850 self.result.stopTest(test)
842851
=== modified file 'python/subunit/tests/test_subunit_filter.py'
--- python/subunit/tests/test_subunit_filter.py 2009-03-08 20:34:19 +0000
+++ python/subunit/tests/test_subunit_filter.py 2009-08-05 06:57:48 +0000
@@ -91,6 +91,19 @@
91 self.filtered_result.failures])91 self.filtered_result.failures])
92 self.assertEqual(5, self.filtered_result.testsRun)92 self.assertEqual(5, self.filtered_result.testsRun)
9393
94 def test_filter_predicate(self):
95 """You can filter by predicate callbacks"""
96 self.filtered_result = unittest.TestResult()
97 filter_cb = lambda test, err: str(err).find('error details') != -1
98 self.filter = subunit.TestResultFilter(self.filtered_result,
99 filter_predicate=filter_cb,
100 filter_success=False)
101 self.run_tests()
102 self.assertEqual(1,
103 self.filtered_result.testsRun)
104 # I'd like to test filtering the xfail but it's blocked by
105 # https://bugs.edge.launchpad.net/subunit/+bug/409193 -- mbp 20090805
106
94 def run_tests(self):107 def run_tests(self):
95 self.setUpTestStream()108 self.setUpTestStream()
96 self.test = subunit.ProtocolTestCase(self.input_stream)109 self.test = subunit.ProtocolTestCase(self.input_stream)
@@ -109,7 +122,9 @@
109tags: local122tags: local
110failure failed123failure failed
111test error124test error
112error error125error error [
126error details
127]
113test skipped128test skipped
114skip skipped129skip skipped
115test todo130test todo

Subscribers

People subscribed via source and target branches