Merge lp:~jml/testtools/more-content-convenience into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merge reported by: Jonathan Lange
Merged at revision: not available
Proposed branch: lp:~jml/testtools/more-content-convenience
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 545 lines (+293/-43)
9 files modified
NEWS (+4/-0)
doc/for-test-authors.rst (+6/-6)
testtools/compat.py (+13/-9)
testtools/content.py (+115/-1)
testtools/deferredruntest.py (+1/-3)
testtools/tests/test_content.py (+146/-17)
testtools/tests/test_run.py (+5/-4)
testtools/tests/test_testresult.py (+1/-3)
testtools/tests/test_testtools.py (+2/-0)
To merge this branch: bzr merge lp:~jml/testtools/more-content-convenience
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+44870@code.launchpad.net

Description of the change

This branch adds a bunch of convenience methods for the content APIs.

Specifically,
 * Content.from_file, which makes a Content object based on file contents
 * Content.from_stream, which makes a Content object based on stream contents
 * TestCase.attachFile(name, *args), which is TestCase.addDetail(name, Content.from_file(*args))

I've also made Content.from_text, and made the text_content function a simple pointer to that.

There's some duplication between from_file and from_stream. Not sure what to do about that.

I added the eager loading (aka read_now) option because there's code in Launchpad that explicitly does eager loading in order to avoid bad interaction between fixture tear down and addDetail. Not sure if it should be the default.

I also don't know what the default chunk size should be. The value I've chosen is almost certainly wrong.

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

Meta: I think these would be better as stand alone functions rather
than class methods.

>  * TestCase.attachFile(name, *args), which is TestCase.addDetail(name, Content.from_file(*args))

I worry a little about expanding the TestCase contract; I won't block
this, but I think it would be clearer not to have an attachFile
method.

> I've also made Content.from_text, and made the text_content function a simple pointer to that.
>
> There's some duplication between from_file and from_stream. Not sure what to do about that.

I would address that by having a 'stream' fixture handed into a common
function, and both delegate to that - one can use a trivial lambda,
the other case opens a file and closes it.

Theres a lifetime ambiguity about from_stream - except for StringIO,
who will close the stream? We should document that, and how to handle
it.

The eager loading thing is fine I think, except that all the other
logic becomes irrelevant so it really should be a separate codepath
for the file case (because chunking on read-now is pointless), and the
stream case can be ignored - clients can call .read() instead - that
will be clearer.

I haven't looked at the tests in detail, I'm sure they will be fine
supporting cases for the changes you've made.

-Rob

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

 * I've dropped attachFile. We can add it later if the people clamor for it.

 * I don't think they'd be better as stand-alone functions. At the very least, standalone functions can be implemented in terms of classmethods (e.g. content_from_file = Content.from_file), but classmethods can only be built from functions if they drop the class-based dispatch. As far as I can tell, it's a matter of taste. I am willing to budge on this one.

 * I've added documentation to from_stream saying that it will close the stream when the iterator is exhausted, and made it actually close the stream. The reasoning is that testtools doesn't really offer any sort of hook for a sensible place to close the stream.

 * I've dropped the read_now path from from_stream.

 * I've changed the read_now path in from_file to be completely separate from the lazy read path.

 * I couldn't get the fixture thing to work.

Unfortunately, without attachFile, we still don't have a nice syntax for the common case of "add this file as a detail during teardown". The best we have is:
  self.addCleanup(lambda: self.addDetail('foo', Content.from_file('foo.txt', read_now=True)))

As opposed to:
  self.addCleanup(self.attachFile, 'foo', 'foo.txt', read_now=True)

I'm going to go away and think some more.

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

Will look at the code todayish.

On Fri, Dec 31, 2010 at 3:38 AM, Jonathan Lange <email address hidden> wrote:
>  * I've dropped attachFile. We can add it later if the people clamor for it.
>
>  * I don't think they'd be better as stand-alone functions.  At the very least, standalone functions can be implemented in terms of classmethods (e.g. content_from_file = Content.from_file), but classmethods can only be built from functions if they drop the class-based dispatch.  As far as I can tell, it's a matter of taste.  I am willing to budge on this one.

I think doing 'TracebackContent.from_file' would be hugely confusing.
I'll see about whipping up a comparitive patch.

>  * I've added documentation to from_stream saying that it will close the stream when the iterator is exhausted, and made it actually close the stream.  The reasoning is that testtools doesn't really offer any sort of hook for a sensible place to close the stream.

>  * I've changed the read_now path in from_file to be completely separate from the lazy read path.
>
>  * I couldn't get the fixture thing to work.

What didn't work?

> Unfortunately, without attachFile, we still don't have a nice syntax for the common case of "add this file as a detail during teardown".  The best we have is:
>  self.addCleanup(lambda: self.addDetail('foo', Content.from_file('foo.txt', read_now=True)))
>
> As opposed to:
>  self.addCleanup(self.attachFile, 'foo', 'foo.txt', read_now=True)

I see, the lambda is needed because we want to force Content.from_file
to execute at cleanup time, not before.

And what we're really saying is 'try to read this thing during
cleanup, and cache the result cause its going to get deleted'.

So there are a few related bits here; the first is that the case I
expect this to happen in, is the case where the file being read is a
log file for a temporary server. I'd expect that server to have a
fixture, and the fixtures /details/ will be automatically captured
during cleanup. So the place that 'attachFile during cleanup' as a
convenience method is most needed is in fixtures. or somewhere where
it is usable by both TestCase and Fixture.

def attach_file(path, obj_with_details, name=None, lazy_read=False):
     """Attach the contents of the file at path to obj_with_details.

     This function adds a 'details' object to obj_with_details which will
     return the content of path. By default the content is read immediately
     and cached.

    :param path: The path of the file.
    :param obj_with_details: The object to attach the file to -
generally a TestCase or Fixture.
    :param name: Optional name for the file. If not specified the
basename is used.
    :param lazy_read: If True the file content is not read when
attach_file is called, but later
        when the content object is evaluated. Note that this may be
after any cleanups that
        obj_with_details has, so if the file is a temporary file
lazy_read may cause the file to
        be read after it is deleted. To handle those cases, using
attach_file as a cleanup is
        recommended::
             obj_with_details.addCleanUp(attach_file, 'foo.txt',
obj_with_details)
    :return: None
    """

179. By Jonathan Lange

Merge trunk.

180. By Jonathan Lange

Copyright bump

181. By Jonathan Lange

Delete from_text.

182. By Jonathan Lange

Move Content.from_stream to be content_from_stream

183. By Jonathan Lange

Fix up the documentation.

184. By Jonathan Lange

Move attachFile from TestCase to be a standalone function.

185. By Jonathan Lange

Change the order of parameters to attach_file

186. By Jonathan Lange

read_now => lazy_read, as per review.

187. By Jonathan Lange

More tests for attach_file, make lazy read work.

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

I couldn't make the code look better by using fixtures. They just made it more cumbersome.

I've changed the classmethods to be functions (although I still think it looks uglier), and I've changed attachFile to be a standalone function too. I still think TestCase would benefit from a convenience method, but this is still a net improvement.

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

I've push up a riff on your branch - lp:~lifeless/testtools/details - if you like that, just land it. Otherwise lets chat on IRC/voice now that I've refreshed the branch in my head.

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

(if you like my riff, land with the riff)

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

In a nutshell,
 * I like content_from_reader
 * I think the decorator-for-default-args things is kind of neat
 * But I also think it's a bit too clever (it is also missing tests)

Will probably land with your riff sans decorator.

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

Riff (lp:~lifeless/testtools/details) landed sans decorator.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2011-01-22 16:47:31 +0000
+++ NEWS 2011-02-01 18:58:11 +0000
@@ -24,6 +24,10 @@
2424
25* ``MultiTestResult`` now documented in the manual. (Jonathan Lange, #661116)25* ``MultiTestResult`` now documented in the manual. (Jonathan Lange, #661116)
2626
27* New content helpers ``content_from_file``, ``content_from_stream`` and
28 ``attach_file`` make it easier to attach file-like objects to a
29 test. (Jonathan Lange, #694126)
30
27* New ``ExpectedException`` context manager to help write tests against things31* New ``ExpectedException`` context manager to help write tests against things
28 that are expected to raise exceptions. (Aaron Bentley)32 that are expected to raise exceptions. (Aaron Bentley)
2933
3034
=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst 2011-01-22 16:47:31 +0000
+++ doc/for-test-authors.rst 2011-02-01 18:58:11 +0000
@@ -737,6 +737,10 @@
737737
738 image = Content(ContentType('image', 'png'), lambda: open('foo.png').read())738 image = Content(ContentType('image', 'png'), lambda: open('foo.png').read())
739739
740Or you could use the convenience function::
741
742 image = content_from_file('foo.png', ContentType('image', 'png'))
743
740The ``lambda`` helps make sure that the file is opened and the actual bytes744The ``lambda`` helps make sure that the file is opened and the actual bytes
741read only when they are needed – by default, when the test is finished. This745read only when they are needed – by default, when the test is finished. This
742means that tests can construct and add Content objects freely without worrying746means that tests can construct and add Content objects freely without worrying
@@ -754,7 +758,7 @@
754might do it::758might do it::
755759
756 from testtools import TestCase760 from testtools import TestCase
757 from testtools.content import Content761 from testtools.content import attach_file, Content
758 from testtools.content_type import UTF8_TEXT762 from testtools.content_type import UTF8_TEXT
759763
760 from myproject import SomeServer764 from myproject import SomeServer
@@ -766,7 +770,7 @@
766 self.server = SomeServer()770 self.server = SomeServer()
767 self.server.start_up()771 self.server.start_up()
768 self.addCleanup(self.server.shut_down)772 self.addCleanup(self.server.shut_down)
769 self.addCleanup(self.attach_log_file)773 self.addCleanup(attach_file, self.server.logfile, self)
770774
771 def attach_log_file(self):775 def attach_log_file(self):
772 self.addDetail(776 self.addDetail(
@@ -781,10 +785,6 @@
781run. testtools will only display the log file for failing tests, so it's not785run. testtools will only display the log file for failing tests, so it's not
782such a big deal.786such a big deal.
783787
784Note the callable passed to ``Content`` reads the log file line-by-line. This
785is something of an optimization. If we naively returned all of the log file
786as one bytestring, ``Content`` would treat that as a list of byte chunks.
787
788If the act of adding at detail is expensive, you might want to use788If the act of adding at detail is expensive, you might want to use
789addOnException_ so that you only do it when a test actually raises an789addOnException_ so that you only do it when a test actually raises an
790exception.790exception.
791791
=== modified file 'testtools/compat.py'
--- testtools/compat.py 2011-01-22 17:56:00 +0000
+++ testtools/compat.py 2011-02-01 18:58:11 +0000
@@ -2,24 +2,28 @@
22
3"""Compatibility support for python 2 and 3."""3"""Compatibility support for python 2 and 3."""
44
5
6import codecs
7import linecache
8import locale
9import os
10import re
11import sys
12import traceback
13
14__metaclass__ = type5__metaclass__ = type
15__all__ = [6__all__ = [
16 '_b',7 '_b',
17 '_u',8 '_u',
18 'advance_iterator',9 'advance_iterator',
19 'str_is_unicode',10 'str_is_unicode',
11 'StringIO',
20 'unicode_output_stream',12 'unicode_output_stream',
21 ]13 ]
2214
15import codecs
16import linecache
17import locale
18import os
19import re
20import sys
21import traceback
22
23from testtools.helpers import try_imports
24
25StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
26
2327
24__u_doc = """A function version of the 'u' prefix.28__u_doc = """A function version of the 'u' prefix.
2529
2630
=== modified file 'testtools/content.py'
--- testtools/content.py 2011-01-22 17:56:00 +0000
+++ testtools/content.py 2011-02-01 18:58:11 +0000
@@ -1,8 +1,18 @@
1# Copyright (c) 2009-2010 testtools developers. See LICENSE for details.1# Copyright (c) 2009-2011 testtools developers. See LICENSE for details.
22
3"""Content - a MIME-like Content object."""3"""Content - a MIME-like Content object."""
44
5__all__ = [
6 'attach_file',
7 'Content',
8 'content_from_file',
9 'content_from_stream',
10 'text_content',
11 'TracebackContent',
12 ]
13
5import codecs14import codecs
15import os
616
7from testtools.compat import _b17from testtools.compat import _b
8from testtools.content_type import ContentType, UTF8_TEXT18from testtools.content_type import ContentType, UTF8_TEXT
@@ -12,6 +22,21 @@
12_join_b = _b("").join22_join_b = _b("").join
1323
1424
25DEFAULT_CHUNK_SIZE = 4096
26
27
28def _iter_chunks(stream, chunk_size):
29 """Read 'stream' in chunks of 'chunk_size'.
30
31 :param stream: A file-like object to read from.
32 :param chunk_size: The size of each read from 'stream'.
33 """
34 chunk = stream.read(chunk_size)
35 while chunk:
36 yield chunk
37 chunk = stream.read(chunk_size)
38
39
15class Content(object):40class Content(object):
16 """A MIME-like Content object.41 """A MIME-like Content object.
1742
@@ -100,3 +125,92 @@
100 This is useful for adding details which are short strings.125 This is useful for adding details which are short strings.
101 """126 """
102 return Content(UTF8_TEXT, lambda: [text.encode('utf8')])127 return Content(UTF8_TEXT, lambda: [text.encode('utf8')])
128
129
130def content_from_file(path, content_type=None, chunk_size=None,
131 lazy_read=True):
132 """Create a `Content` object from a file on disk.
133
134 Note that unless 'read_now' is explicitly passed in as True, the file
135 will only be read from when ``iter_bytes`` is called.
136
137 :param path: The path to the file to be used as content.
138 :param content_type: The type of content. If not specified, defaults
139 to UTF8-encoded text/plain.
140 :param chunk_size: The size of chunks to read from the file.
141 Defaults to `DEFAULT_CHUNK_SIZE`.
142 :param lazy_read: If False, read the file from disk now and keep it in
143 memory. Otherwise, only read when the content is serialized.
144 """
145 if content_type is None:
146 content_type = UTF8_TEXT
147 if chunk_size is None:
148 chunk_size = DEFAULT_CHUNK_SIZE
149 def reader():
150 stream = open(path, 'rb')
151 for chunk in _iter_chunks(stream, chunk_size):
152 yield chunk
153 stream.close()
154 if not lazy_read:
155 contents = list(reader())
156 reader = lambda: contents
157 return Content(content_type, reader)
158
159
160def content_from_stream(stream, content_type=None, chunk_size=None,
161 lazy_read=True):
162 """Create a `Content` object from a file-like stream.
163
164 Note that the stream will only be read from when ``iter_bytes`` is
165 called.
166
167 :param stream: A file-like object to read the content from.
168 :param content_type: The type of content. If not specified, defaults
169 to UTF8-encoded text/plain.
170 :param chunk_size: The size of chunks to read from the file.
171 Defaults to `DEFAULT_CHUNK_SIZE`.
172 :param lazy_read: If False, reads from the stream right now. Otherwise,
173 only reads when the content is serialized. Defaults to True.
174 """
175 if content_type is None:
176 content_type = UTF8_TEXT
177 if chunk_size is None:
178 chunk_size = DEFAULT_CHUNK_SIZE
179 reader = lambda: _iter_chunks(stream, chunk_size)
180 if not lazy_read:
181 contents = list(reader())
182 reader = lambda: contents
183 return Content(content_type, reader)
184
185
186def attach_file(detailed, path, name=None, content_type=None,
187 chunk_size=None, lazy_read=False):
188 """Attach a file to this test as a detail.
189
190 This is a convenience method wrapping around `addDetail`.
191
192 Note that unless 'read_now' is explicitly passed in as True, the file
193 *must* exist when the test result is called with the results of this
194 test, after the test has been torn down.
195
196 :param detailed: An object with details
197 :param path: The path to the file to attach.
198 :param name: The name to give to the detail for the attached file.
199 :param content_type: The content type of the file. If not provided,
200 defaults to UTF8-encoded text/plain.
201 :param chunk_size: The size of chunks to read from the file. Defaults
202 to something sensible.
203 :param lazy_read: If True the file content is not read when attach_file is
204 called, but later when the content object is evaluated. Note that this
205 may be after any cleanups that obj_with_details has, so if the file is
206 a temporary file lazy_read may cause the file to be read after it is
207 deleted. To handle those cases, using attach_file as a cleanup is
208 recommended::
209
210 detailed.addCleanup(attach_file, 'foo.txt', detailed)
211 """
212 if name is None:
213 name = os.path.basename(os.path.abspath(path))
214 content_object = content_from_file(
215 path, content_type, chunk_size, lazy_read)
216 detailed.addDetail(name, content_object)
103217
=== modified file 'testtools/deferredruntest.py'
--- testtools/deferredruntest.py 2011-01-22 17:56:00 +0000
+++ testtools/deferredruntest.py 2011-02-01 18:58:11 +0000
@@ -15,7 +15,7 @@
1515
16import sys16import sys
1717
18from testtools import try_imports18from testtools.compat import StringIO
19from testtools.content import (19from testtools.content import (
20 Content,20 Content,
21 text_content,21 text_content,
@@ -34,8 +34,6 @@
34from twisted.python import log34from twisted.python import log
35from twisted.trial.unittest import _LogObserver35from twisted.trial.unittest import _LogObserver
3636
37StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
38
3937
40class _DeferredRunTest(RunTest):38class _DeferredRunTest(RunTest):
41 """Base for tests that return Deferreds."""39 """Base for tests that return Deferreds."""
4240
=== modified file 'testtools/tests/test_content.py'
--- testtools/tests/test_content.py 2011-01-22 17:56:00 +0000
+++ testtools/tests/test_content.py 2011-02-01 18:58:11 +0000
@@ -1,11 +1,33 @@
1# Copyright (c) 2008-2010 testtools developers. See LICENSE for details.1# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
22
3import os
4import tempfile
3import unittest5import unittest
6
4from testtools import TestCase7from testtools import TestCase
5from testtools.compat import _b, _u8from testtools.compat import (
6from testtools.content import Content, TracebackContent, text_content9 _b,
7from testtools.content_type import ContentType, UTF8_TEXT10 _u,
8from testtools.matchers import MatchesException, Raises11 StringIO,
12 )
13from testtools.content import (
14 attach_file,
15 Content,
16 content_from_file,
17 content_from_stream,
18 TracebackContent,
19 text_content,
20 )
21from testtools.content_type import (
22 ContentType,
23 UTF8_TEXT,
24 )
25from testtools.matchers import (
26 Equals,
27 MatchesException,
28 Raises,
29 raises,
30 )
9from testtools.tests.helpers import an_exc_info31from testtools.tests.helpers import an_exc_info
1032
1133
@@ -15,10 +37,11 @@
15class TestContent(TestCase):37class TestContent(TestCase):
1638
17 def test___init___None_errors(self):39 def test___init___None_errors(self):
18 self.assertThat(lambda:Content(None, None), raises_value_error)40 self.assertThat(lambda: Content(None, None), raises_value_error)
19 self.assertThat(lambda:Content(None, lambda: ["traceback"]),41 self.assertThat(
20 raises_value_error)42 lambda: Content(None, lambda: ["traceback"]), raises_value_error)
21 self.assertThat(lambda:Content(ContentType("text", "traceback"), None),43 self.assertThat(
44 lambda: Content(ContentType("text", "traceback"), None),
22 raises_value_error)45 raises_value_error)
2346
24 def test___init___sets_ivars(self):47 def test___init___sets_ivars(self):
@@ -64,12 +87,67 @@
64 content = Content(content_type, lambda: [iso_version])87 content = Content(content_type, lambda: [iso_version])
65 self.assertEqual([text], list(content.iter_text()))88 self.assertEqual([text], list(content.iter_text()))
6689
90 def test_from_file(self):
91 fd, path = tempfile.mkstemp()
92 self.addCleanup(os.remove, path)
93 os.write(fd, 'some data')
94 os.close(fd)
95 content = content_from_file(path, UTF8_TEXT, chunk_size=2)
96 self.assertThat(
97 list(content.iter_bytes()), Equals(['so', 'me', ' d', 'at', 'a']))
98
99 def test_from_nonexistent_file(self):
100 directory = tempfile.mkdtemp()
101 nonexistent = os.path.join(directory, 'nonexistent-file')
102 content = content_from_file(nonexistent)
103 self.assertThat(content.iter_bytes, raises(IOError))
104
105 def test_from_file_default_type(self):
106 content = content_from_file('/nonexistent/path')
107 self.assertThat(content.content_type, Equals(UTF8_TEXT))
108
109 def test_from_file_eager_loading(self):
110 fd, path = tempfile.mkstemp()
111 os.write(fd, 'some data')
112 os.close(fd)
113 content = content_from_file(path, UTF8_TEXT, lazy_read=False)
114 os.remove(path)
115 self.assertThat(
116 _b('').join(content.iter_bytes()), Equals('some data'))
117
118 def test_from_stream(self):
119 data = StringIO('some data')
120 content = content_from_stream(data, UTF8_TEXT, chunk_size=2)
121 self.assertThat(
122 list(content.iter_bytes()), Equals(['so', 'me', ' d', 'at', 'a']))
123
124 def test_from_stream_default_type(self):
125 data = StringIO('some data')
126 content = content_from_stream(data)
127 self.assertThat(content.content_type, Equals(UTF8_TEXT))
128
129 def test_from_stream_eager_loading(self):
130 fd, path = tempfile.mkstemp()
131 self.addCleanup(os.remove, path)
132 os.write(fd, 'some data')
133 stream = open(path, 'rb')
134 content = content_from_stream(stream, UTF8_TEXT, lazy_read=False)
135 os.write(fd, 'more data')
136 os.close(fd)
137 self.assertThat(
138 _b('').join(content.iter_bytes()), Equals('some data'))
139
140 def test_from_text(self):
141 data = _u("some data")
142 expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')])
143 self.assertEqual(expected, text_content(data))
144
67145
68class TestTracebackContent(TestCase):146class TestTracebackContent(TestCase):
69147
70 def test___init___None_errors(self):148 def test___init___None_errors(self):
71 self.assertThat(lambda:TracebackContent(None, None),149 self.assertThat(
72 raises_value_error) 150 lambda: TracebackContent(None, None), raises_value_error)
73151
74 def test___init___sets_ivars(self):152 def test___init___sets_ivars(self):
75 content = TracebackContent(an_exc_info, self)153 content = TracebackContent(an_exc_info, self)
@@ -81,12 +159,63 @@
81 self.assertEqual(expected, ''.join(list(content.iter_text())))159 self.assertEqual(expected, ''.join(list(content.iter_text())))
82160
83161
84class TestBytesContent(TestCase):162class TestAttachFile(TestCase):
85163
86 def test_bytes(self):164 def make_file(self, data):
87 data = _u("some data")165 fd, path = tempfile.mkstemp()
88 expected = Content(UTF8_TEXT, lambda: [data.encode('utf8')])166 self.addCleanup(os.remove, path)
89 self.assertEqual(expected, text_content(data))167 os.write(fd, data)
168 os.close(fd)
169 return path
170
171 def test_simple(self):
172 class SomeTest(TestCase):
173 def test_foo(self):
174 pass
175 test = SomeTest('test_foo')
176 data = 'some data'
177 path = self.make_file(data)
178 my_content = text_content(data)
179 attach_file(test, path, name='foo')
180 self.assertEqual({'foo': my_content}, test.getDetails())
181
182 def test_optional_name(self):
183 # If no name is provided, attach_file just uses the base name of the
184 # file.
185 class SomeTest(TestCase):
186 def test_foo(self):
187 pass
188 test = SomeTest('test_foo')
189 path = self.make_file('some data')
190 base_path = os.path.basename(path)
191 attach_file(test, path)
192 self.assertEqual([base_path], list(test.getDetails()))
193
194 def test_lazy_read(self):
195 class SomeTest(TestCase):
196 def test_foo(self):
197 pass
198 test = SomeTest('test_foo')
199 path = self.make_file('some data')
200 attach_file(test, path, name='foo', lazy_read=True)
201 content = test.getDetails()['foo']
202 content_file = open(path, 'w')
203 content_file.write('new data')
204 content_file.close()
205 self.assertEqual(''.join(content.iter_bytes()), 'new data')
206
207 def test_eager_read_by_default(self):
208 class SomeTest(TestCase):
209 def test_foo(self):
210 pass
211 test = SomeTest('test_foo')
212 path = self.make_file('some data')
213 attach_file(test, path, name='foo')
214 content = test.getDetails()['foo']
215 content_file = open(path, 'w')
216 content_file.write('new data')
217 content_file.close()
218 self.assertEqual(''.join(content.iter_bytes()), 'some data')
90219
91220
92def test_suite():221def test_suite():
93222
=== modified file 'testtools/tests/test_run.py'
--- testtools/tests/test_run.py 2011-01-22 17:56:00 +0000
+++ testtools/tests/test_run.py 2011-02-01 18:58:11 +0000
@@ -2,9 +2,9 @@
22
3"""Tests for the test runner logic."""3"""Tests for the test runner logic."""
44
5from testtools.helpers import try_import, try_imports5from testtools.compat import StringIO
6from testtools.helpers import try_import
6fixtures = try_import('fixtures')7fixtures = try_import('fixtures')
7StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
88
9import testtools9import testtools
10from testtools import TestCase, run10from testtools import TestCase, run
@@ -41,7 +41,7 @@
41 def test_run_list(self):41 def test_run_list(self):
42 if fixtures is None:42 if fixtures is None:
43 self.skipTest("Need fixtures")43 self.skipTest("Need fixtures")
44 package = self.useFixture(SampleTestFixture())44 self.useFixture(SampleTestFixture())
45 out = StringIO()45 out = StringIO()
46 run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)46 run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)
47 self.assertEqual("""testtools.runexample.TestFoo.test_bar47 self.assertEqual("""testtools.runexample.TestFoo.test_bar
@@ -51,7 +51,7 @@
51 def test_run_load_list(self):51 def test_run_load_list(self):
52 if fixtures is None:52 if fixtures is None:
53 self.skipTest("Need fixtures")53 self.skipTest("Need fixtures")
54 package = self.useFixture(SampleTestFixture())54 self.useFixture(SampleTestFixture())
55 out = StringIO()55 out = StringIO()
56 # We load two tests - one that exists and one that doesn't, and we56 # We load two tests - one that exists and one that doesn't, and we
57 # should get the one that exists and neither the one that doesn't nor57 # should get the one that exists and neither the one that doesn't nor
@@ -71,6 +71,7 @@
71 self.assertEqual("""testtools.runexample.TestFoo.test_bar71 self.assertEqual("""testtools.runexample.TestFoo.test_bar
72""", out.getvalue())72""", out.getvalue())
7373
74
74def test_suite():75def test_suite():
75 from unittest import TestLoader76 from unittest import TestLoader
76 return TestLoader().loadTestsFromName(__name__)77 return TestLoader().loadTestsFromName(__name__)
7778
=== modified file 'testtools/tests/test_testresult.py'
--- testtools/tests/test_testresult.py 2011-01-22 17:56:00 +0000
+++ testtools/tests/test_testresult.py 2011-02-01 18:58:11 +0000
@@ -22,7 +22,6 @@
22 TextTestResult,22 TextTestResult,
23 ThreadsafeForwardingResult,23 ThreadsafeForwardingResult,
24 testresult,24 testresult,
25 try_imports,
26 )25 )
27from testtools.compat import (26from testtools.compat import (
28 _b,27 _b,
@@ -30,6 +29,7 @@
30 _r,29 _r,
31 _u,30 _u,
32 str_is_unicode,31 str_is_unicode,
32 StringIO,
33 )33 )
34from testtools.content import Content34from testtools.content import Content
35from testtools.content_type import ContentType, UTF8_TEXT35from testtools.content_type import ContentType, UTF8_TEXT
@@ -47,8 +47,6 @@
47 )47 )
48from testtools.testresult.real import utc48from testtools.testresult.real import utc
4949
50StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
51
5250
53class Python26Contract(object):51class Python26Contract(object):
5452
5553
=== modified file 'testtools/tests/test_testtools.py'
--- testtools/tests/test_testtools.py 2011-01-22 17:56:00 +0000
+++ testtools/tests/test_testtools.py 2011-02-01 18:58:11 +0000
@@ -3,7 +3,9 @@
3"""Tests for extensions to the base test library."""3"""Tests for extensions to the base test library."""
44
5from pprint import pformat5from pprint import pformat
6import os
6import sys7import sys
8import tempfile
7import unittest9import unittest
810
9from testtools import (11from testtools import (

Subscribers

People subscribed via source and target branches