Merge lp:~frankban/python-fixtures/fakelogger-getdetails into lp:~python-fixtures/python-fixtures/trunk

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 51
Proposed branch: lp:~frankban/python-fixtures/fakelogger-getdetails
Merge into: lp:~python-fixtures/python-fixtures/trunk
Diff against target: 117 lines (+24/-10)
3 files modified
lib/fixtures/_fixtures/logger.py (+9/-3)
lib/fixtures/fixture.py (+5/-5)
lib/fixtures/tests/_fixtures/test_logger.py (+10/-2)
To merge this branch: bzr merge lp:~frankban/python-fixtures/fakelogger-getdetails
Reviewer Review Type Date Requested Status
python-fixtures committers Pending
Review via email: mp+108593@code.launchpad.net

Description of the change

Following Robert suggestion in his comment to bug 1001520, I've used the FakeLogger fixture to capture Memcached logs in tests generating warnings. This branch implements FakeLogger.getDetails, so that we can still retrieve logging info if one of those tests fails.
I was confused about what name to use for the detail, I ended up using the FakeLogger instance class name, but I am open to suggestions.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Thanks for this patch.

There seem to be a lot of whitespace changes in the diff? Possibly
harmless, but confusing.

Rather than implementing getDetails, the normal mechanism is to call
self.addDetail with a detail that will return the appropriate content
when evaluated. This is cheaper than evaluating when getDetails is
called, and means you only need to test that an appropriate detail is
added, rather than that the getDetails method still Does The Right
Thing. The UTF8_TEXT helper is probably revelant here too, for
brevity.

Finally, on the naming of things, I'd describe the attachment - think
of it as an email attachment.

So, in summary - something like:

E.g.:

=== modified file 'lib/fixtures/_fixtures/logger.py'
--- lib/fixtures/_fixtures/logger.py 2011-11-25 17:37:36 +0000
+++ lib/fixtures/_fixtures/logger.py 2012-06-04 20:16:20 +0000
@@ -18,6 +18,10 @@

 from fixtures import Fixture

+from testtools.content import Content
+from testtools.content_type import UTF8_TEXT
+
 __all__ = [
     'FakeLogger',
     'LoggerFixture',
@@ -52,7 +56,10 @@

     def setUp(self):
         super(FakeLogger, self).setUp()
- self._output = StringIO()
+ output = StringIO()
+ self._output = output
+ self.addDetail(u"pythonlogging:'%s'" % self._name,
+ Content(UTF8_TEXT, lambda: [output]))
         logger = getLogger(self._name)
         if self._level:
             self.addCleanup(logger.setLevel, logger.level)

Note the use of a closure here so that d = f.getDetails(),
f.cleanUp(), f.setUp(), d.items()[0].itertext() will not return the
new (empty) state of the fixture.

HTH,
Rob

52. By Francesco Banconi

Fixes from review.

Revision history for this message
Francesco Banconi (frankban) wrote :

> There seem to be a lot of whitespace changes in the diff? Possibly
> harmless, but confusing.

My editor is set up to strip spaces at the end of lines when I save a file.
I can revert changes in fixture.py if you like, I should have mentioned this earlier, sorry.

> Rather than implementing getDetails, the normal mechanism is to call
> self.addDetail with a detail that will return the appropriate content
> when evaluated. This is cheaper than evaluating when getDetails is
> called, and means you only need to test that an appropriate detail is
> added, rather than that the getDetails method still Does The Right
> Thing. The UTF8_TEXT helper is probably revelant here too, for
> brevity.

Great! I've changed the branch following this suggestion, and deleted a test.

> Finally, on the naming of things, I'd describe the attachment - think
> of it as an email attachment.

Now the detail name is an instance attribute: if you don't like this, I can just
format the string in setUp and find another way (e.g. .values()[0]) to get the
content in the test.

> Note the use of a closure here so that d = f.getDetails(),
> f.cleanUp(), f.setUp(), d.items()[0].itertext() will not return the
> new (empty) state of the fixture.

Yes, I was using a closure in the previous revision too. It's cool and, AFAICT,
lazy evaluation is the only option here, since details are gathered early in useFixture:
http://bazaar.launchpad.net/~testtools-committers/testtools/trunk/view/head:/testtools/testcase.py#L585

Thanks Robert.

Revision history for this message
Robert Collins (lifeless) wrote :

On Tue, Jun 5, 2012 at 10:14 PM, Francesco Banconi
<email address hidden> wrote:
>> There seem to be a lot of whitespace changes in the diff? Possibly
>> harmless, but confusing.
>
> My editor is set up to strip spaces at the end of lines when I save a file.
> I can revert changes in fixture.py if you like, I should have mentioned this earlier, sorry.

No probs, thanks for clarifying.

>> Rather than implementing getDetails, the normal mechanism is to call
>> self.addDetail with a detail that will return the appropriate content
>> when evaluated. This is cheaper than evaluating when getDetails is
>> called, and means you only need to test that an appropriate detail is
>> added, rather than that the getDetails method still Does The Right
>> Thing. The UTF8_TEXT helper is probably revelant here too, for
>> brevity.
>
> Great! I've changed the branch following this suggestion, and deleted a test.
>
>> Finally, on the naming of things, I'd describe the attachment - think
>> of it as an email attachment.
>
> Now the detail name is an instance attribute: if you don't like this, I can just
> format the string in setUp and find another way (e.g. .values()[0]) to get the
> content in the test.

I don't, because with name merging the and subordinate fixtures folk
will generally be unable to rely on such an instance attribute being
correct. Documenting how the name is ordinarily calculated, and then
using a literal string in the test would be fine, I think.

>> Note the use of a closure here so that d = f.getDetails(),
>> f.cleanUp(), f.setUp(), d.items()[0].itertext() will not return the
>> new (empty) state of the fixture.
>
> Yes, I was using a closure in the previous revision too. It's cool and, AFAICT,
> lazy evaluation is the only option here, since details are gathered early in useFixture:
> http://bazaar.launchpad.net/~testtools-committers/testtools/trunk/view/head:/testtools/testcase.py#L585

I'll look at the exact changes when I'm less sick :(. The previous
version used a closure of lambda: [self.output], but self.output is a
property, so it does not have the behaviour I described. When
evaluated it would get the current valuation from self, not the
valuation of the log output for the time when the fixture was first
setup.

-Rob

53. By Francesco Banconi

Updated the way the detail name is added.

Revision history for this message
Francesco Banconi (frankban) wrote :

Code updated.

Revision history for this message
Jonathan Lange (jml) wrote :

testtools.content.content_from_stream might be slightly better than Content(UTF8_TEXT, ...) for verbose logs, as it reads the content in chunks. If not t.c.text_content does almost the same thing as the current code and is briefer.

It's not clear to me *when* you want to do the read. Presumably it's when getDetails is called, which seems to be what the current version does (and what content_from_stream would do). I don't really understand Robert's last paragraph.

54. By Francesco Banconi

Removed unused text_content import.

Revision history for this message
Francesco Banconi (frankban) wrote :

> testtools.content.content_from_stream might be slightly better than
> Content(UTF8_TEXT, ...) for verbose logs, as it reads the content in chunks.
> If not t.c.text_content does almost the same thing as the current code and is
> briefer.

I think we want lazy evaluation of content: we don't want to read the stream when getDetails() is called.
As written above, useFixture() gathers details early, and does it by calling fixture.getDetails(), and at this point I expect the stream to be empty. So, if I am not wrong, `text_content` is not an option here, since it requires a string. content_from_stream is a great suggestion, thanks, and evaluates the content later, when `_iter_chunks` is called. Unfortunately to obtain something useful form the StringIO we should also call seek(0) on the stream before generating chunks. That's the reason why I am using `StringIO.getvalue` to get the content.

> It's not clear to me *when* you want to do the read.

I hope I explained the *when* above.

> Presumably it's when
> getDetails is called, which seems to be what the current version does (and
> what content_from_stream would do). I don't really understand Robert's last
> paragraph.

The current version uses a clusure:
lambda: [self.output]
That is the same as:
lambda: [self._output.getvalue()]

The test in this branch should demonstrate that the evaluation returns the current log content, and not the one from when getDetails was called.

Revision history for this message
Robert Collins (lifeless) wrote :

This should demonstrate the problem. There is no need to iterate on
your branch - I will adjust as I merge:

    def test_no_state_confusion_when_reusing_logging_fixture(self):
        fixture = FakeLogger()
        detailname = "pythonlogging:''"
        with fixture:
            content = fixture.getDetails()[detailname]
            logging.info('some message')
            orig_result = content.as_text()
            self.assertEqual(fixture.output, orig_result)
        with fixture:
            self.assertEqual(orig_result, content.as_text())

Revision history for this message
Francesco Banconi (frankban) wrote :

Ah! I see, thank you Robert.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/fixtures/_fixtures/logger.py'
2--- lib/fixtures/_fixtures/logger.py 2011-11-25 17:37:36 +0000
3+++ lib/fixtures/_fixtures/logger.py 2012-06-07 13:10:51 +0000
4@@ -1,12 +1,12 @@
5 # fixtures: Fixtures with cleanups for testing and convenience.
6 #
7 # Copyright (c) 2011, Robert Collins <robertc@robertcollins.net>
8-#
9+#
10 # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
11 # license at the users choice. A copy of both licenses are available in the
12 # project source as Apache-2.0 and BSD. You may not use this file except in
13 # compliance with one of these two licences.
14-#
15+#
16 # Unless required by applicable law or agreed to in writing, software
17 # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
18 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
19@@ -16,6 +16,9 @@
20 from logging import StreamHandler, getLogger, INFO, Formatter
21 from cStringIO import StringIO
22
23+from testtools.content import Content
24+from testtools.content_type import UTF8_TEXT
25+
26 from fixtures import Fixture
27
28 __all__ = [
29@@ -53,7 +56,10 @@
30 def setUp(self):
31 super(FakeLogger, self).setUp()
32 self._output = StringIO()
33- logger = getLogger(self._name)
34+ self.addDetail(
35+ u"pythonlogging:'%s'" % self._name,
36+ Content(UTF8_TEXT, lambda: [self.output]))
37+ logger = getLogger(self._name)
38 if self._level:
39 self.addCleanup(logger.setLevel, logger.level)
40 logger.setLevel(self._level)
41
42=== modified file 'lib/fixtures/fixture.py'
43--- lib/fixtures/fixture.py 2011-11-25 17:55:19 +0000
44+++ lib/fixtures/fixture.py 2012-06-07 13:10:51 +0000
45@@ -1,12 +1,12 @@
46 # fixtures: Fixtures with cleanups for testing and convenience.
47 #
48 # Copyright (c) 2010, Robert Collins <robertc@robertcollins.net>
49-#
50+#
51 # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
52 # license at the users choice. A copy of both licenses are available in the
53 # project source as Apache-2.0 and BSD. You may not use this file except in
54 # compliance with one of these two licences.
55-#
56+#
57 # Unless required by applicable law or agreed to in writing, software
58 # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
59 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
60@@ -151,7 +151,7 @@
61
62 This does not return the internal dictionary: mutating it will have no
63 effect. If you need to mutate it, just do so directly.
64-
65+
66 :return: Dict from name -> content_object.
67 """
68 result = dict(self._details)
69@@ -163,7 +163,7 @@
70 """Prepare the Fixture for use.
71
72 This should be overridden by most concrete fixtures. When overriding
73- be sure to include self.addCleanup calls to restore the fixture to
74+ be sure to include self.addCleanup calls to restore the fixture to
75 an un-setUp state, so that a single Fixture instance can be reused.
76
77 After setUp is called, the fixture will have one or more attributes
78@@ -218,7 +218,7 @@
79
80 Typically used when an existing object or function interface exists but you
81 wish to use it as a Fixture (e.g. because fixtures are in use in your test
82- suite and this will fit in better).
83+ suite and this will fit in better).
84
85 To adapt an object with differently named setUp and cleanUp methods:
86 fixture = FunctionFixture(object.install, object.__class__.remove)
87
88=== modified file 'lib/fixtures/tests/_fixtures/test_logger.py'
89--- lib/fixtures/tests/_fixtures/test_logger.py 2011-11-25 17:37:36 +0000
90+++ lib/fixtures/tests/_fixtures/test_logger.py 2012-06-07 13:10:51 +0000
91@@ -1,12 +1,12 @@
92 # fixtures: Fixtures with cleanups for testing and convenience.
93 #
94 # Copyright (c) 2011, Robert Collins <robertc@robertcollins.net>
95-#
96+#
97 # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
98 # license at the users choice. A copy of both licenses are available in the
99 # project source as Apache-2.0 and BSD. You may not use this file except in
100 # compliance with one of these two licences.
101-#
102+#
103 # Unless required by applicable law or agreed to in writing, software
104 # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
105 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
106@@ -76,3 +76,11 @@
107 self.useFixture(fixture)
108 logging.info("message")
109 self.assertEqual("test_logger\n", fixture.output)
110+
111+ def test_logs_returned_as_details(self):
112+ # Ensure logs are correctly returned as fixture details.
113+ fixture = self.useFixture(FakeLogger())
114+ details = fixture.getDetails()
115+ logging.info('some message')
116+ content = details["pythonlogging:''"]
117+ self.assertEqual(fixture.output, content.as_text())

Subscribers

People subscribed via source and target branches