Merge lp:~vila/bzr/321320-isolate-doc-tests into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 5583
Proposed branch: lp:~vila/bzr/321320-isolate-doc-tests
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/cleanup
Diff against target: 395 lines (+244/-63)
4 files modified
bzrlib/tests/__init__.py (+110/-58)
bzrlib/tests/test_selftest.py (+122/-3)
doc/developers/testing.txt (+5/-2)
doc/en/release-notes/bzr-2.3.txt (+7/-0)
To merge this branch: bzr merge lp:~vila/bzr/321320-isolate-doc-tests
Reviewer Review Type Date Requested Status
Andrew Bennetts Needs Fixing
Review via email: mp+44230@code.launchpad.net

Commit message

Isolate doctests from os.environ

Description of the change

This isolates doctests from os.environ, fixing bug #321320.

We have a set of variables that we want to be isolated from the external os.environ.

I moved them out of 'Test_case._cleanEnvironment' into a dedicated 'isolated_environ' to be able to reuse them.

I then created a override_os_environ/restore_environ pair of *functions* so I can use them as parameter to the DocTestSuite *function* (yes, it's not a class). These functions are then added to the doctest (not to the suite) as a setUp/tearDown pair.

I've thought about being even more radical here by modifying the selftest *command* to save/restore os.environ so that tests can just start with an empty os.environ and install whatever they need, but there are several problems with this approach:
- other test runners won't support it,
- this may trigger weird issues with lazy imports (what if the import is sometimes done before the os.environ is clean and sometimes not, etc),
- this would make it harder to write tests for this code (note that the tests added here rely on being properly isolated. I've commented heavily on that as it's easy to overlook).

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

Some quick notes without getting into a proper review:

Main concern is that I think the doctests need more of bzrlib.tests.TestCase.setUp to be properly isolated, not just the _cleanEnvironment call, or perhaps even TestCaseWithMemoryTransport.setUp equivalent. This would mean pulling a lot more code off the TestCase class so it could be shared. I'm particularly thinking of TestCase._clear_hooks and similar things. Not having cleanUp usable from doctest setUp without clever hacks is particularly annoying.

+ 'http_proxy': None,
+ 'HTTP_PROXY': None,

Scope creepitis: I'd much prefer this was handled by the osutils.set_or_unset_env or similar, platforms without case sensitive environments shouldn't have to do extra work.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> Some quick notes without getting into a proper review:
>
> Main concern is that I think the doctests need more of
> bzrlib.tests.TestCase.setUp to be properly isolated, not just the
> _cleanEnvironment call, or perhaps even TestCaseWithMemoryTransport.setUp
> equivalent. This would mean pulling a lot more code off the TestCase class so
> it could be shared. I'm particularly thinking of TestCase._clear_hooks and
> similar things. Not having cleanUp usable from doctest setUp without clever
> hacks is particularly annoying.

You wouldn't be able to call it from a doctest isn't it ?

But even then, DocTestSuite is a *function* which uses DocTestCase (the class) internally, I'm not against the principle of rewriting python's doctest, but I don't think it's in bzr scope :)

>
>
> + 'http_proxy': None,
> + 'HTTP_PROXY': None,
>
> Scope creepitis: I'd much prefer this was handled by the
> osutils.set_or_unset_env or similar, platforms without case sensitive
> environments shouldn't have to do extra work.

But it's handled this way because on OSes with case sensitive envs, both can be (and are) used !

Revision history for this message
Andrew Bennetts (spiv) wrote :

17 + 'HOME': os.getcwd(),

There's a small behaviour difference here, although I think it's probably ok: this calculates the isolated $HOME once, at module import time, rather that during each test's setUp. Given that tests that care about this probably use TestCaseWithMemoryTransport, which explicitly overrides HOME appropriately, it's probably ok... I wonder a little about the effect on doctests that might be influenced by $HOME that can't use TestCaseWithMemoryTransport. In fact, isn't that what bug 312320 is about? Does this really allow bzrlib.branchbuilder's doctests to run in isolation from whatever the user has in their ~/.bazaar? I can't see how it does.

194 + doc_suite = DocTestSuite(
195 + mod, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)

That's a bit odd: shouldn't both DocTestSuite and REPORT_ONLY_FIRST_FAILURE be prefixed with “doctest.”, or neither of them?

240 + Since we use tests that are already isolated from os.environ abit of care

Typo: “a bit” is two words.

review: Needs Information
Revision history for this message
Andrew Bennetts (spiv) wrote :

My suspicion was right: this branch does not appear to fix 321320, except sometimes by accident.

If you e.g. run 'bzr selftest' from your home dir, HOME is set to your real home, and config values in your normal ~/.bazaar/bazaar.conf like create_signatures take effect. In my case that cases gnome-gpg to be run for every commit the test suite does. At best that will slow down the tests considerably, at worst cause test hangs or test failures. This case definitely ought to work, and if bug 321320 is properly fixed, it would.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

Nice catch !

Thinking more about it, I think the issue is that giving a value to a variable via isolated_environ is just wrong.

If set HOME to None there (but still set it to os.getcwd() for TestCase), I get no failures. I didn't dare before but this makes sense. A test that *requires* HOME to be set for whatever reason, should make sure it's set or fail.

Re-running the test suite without setting HOME *at all* ....<drum roll> pass !

The other variables set here are:

- EMAIL: root cause for the branchbuilder doctest failure, this one is under discussion about whether the *feature* of making whoami mandatory is a good one. Setting it here masked the issue and *users* faced the consequences, I'll leave it for now, but I'm convinced we should set it to None and let the related tests themselves care about it (including the currently hidden failures),
- TERM, LINES, COLUMNS, BZR_COLUMNS: again, the tests needing these settings should set these variables explicitly, we'll get a better tracking of which tests really care about them.

That being said, I'll investigate further and submit a more robust solution, two reviewers agreeing on the same issue is worth addressing isn't it ? :D

Revision history for this message
Vincent Ladeuil (vila) wrote :

So, it turns out that deleting HOME from the environment doesn't
annoy our test suite even slightly, so I did just that. I then
run this branch on babune to see how our various supported OSes
react against homeless tests and the result is: nothing. Not a
single test failed, much to my surprise I should say.

But once you accept that indeed our code and our tests don't need
it, it feels cleaner (and should address spiv's concerns about
GPG, if it doesn't, yell !).

Note that we still have tests that write to $HOME/.bzr.log
(better tracked by running with 'BZR_HOME=. ./bzr selftest'), but
they aren't doctests and as such are out of scope for this fix.

If someone (mgz ?) really feels strongly about adding more
isolation to doctests then feel free to do so, I've prepare
things a bit by adding two functions isolated_doctest_setUp and
isolated_doctest_tearDown that accept a 'test' parameter so it
should be possible to force them to run in a tmpdir.

But since there wasn't any leak I could find that would benefit
from such an isolation, I stopped there.

Now, to come back to spiv's first comment:
> My suspicion was right: this branch does not appear to fix 321320, except sometimes by accident.

> If you e.g. run 'bzr selftest' from your home dir, HOME is set to your real home, and config values in your normal ~/.bazaar/bazaar.conf like create_signatures take effect. In my case that cases gnome-gpg to be run for every commit the test suite does. At best that will slow down the tests considerably, at worst cause test hangs or test failures. This case definitely ought to work, and if bug 321320 is properly fixed, it would.

Would you mind checking this behaviour with bzr trunk ?

And is it fixed now ?

If it isn't, can you point me to the *doctest* you think is exhibiting this behaviour ?

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

I suggest using a fixture (lp:python-fixtures) or similar rather than
a pair of functions. Its a bit cleaner and will be less code
(particularly if you reuse Fixture rather than rolling your own).

Revision history for this message
Martin Packman (gz) wrote :

> If someone (mgz ?) really feels strongly about adding more
> isolation to doctests then feel free to do so, I've prepare
> things a bit by adding two functions isolated_doctest_setUp and
> isolated_doctest_tearDown that accept a 'test' parameter so it
> should be possible to force them to run in a tmpdir.

This may as well land if it fixes the problems with doctests people are actually having, even if it's not a complete solution.

Anyone with for instance, a plugin that adds a hook and doesn't run selftest with --no-plugins will still risk random behaviour though right? I'd like a note in the developer documentation saying doctests are not as robustly isolated as general tests.

Do you also want to uncomment the doctest we disabled the other day in this branch?

Revision history for this message
Vincent Ladeuil (vila) wrote :

> This may as well land if it fixes the problems with doctests people are
> actually having, even if it's not a complete solution.

Ok.

>
> Anyone with for instance, a plugin that adds a hook and doesn't run selftest
> with --no-plugins will still risk random behaviour though right?

Right.

> I'd like a
> note in the developer documentation saying doctests are not as robustly
> isolated as general tests.

Ok.

> Do you also want to uncomment the doctest we disabled the other day in this
> branch?

I did and used it to ensure my fix addressed the related problem.

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-12-26 16:23:16 +0000
+++ bzrlib/tests/__init__.py 2011-01-07 11:18:35 +0000
@@ -123,6 +123,93 @@
123TestSuite = TestUtil.TestSuite123TestSuite = TestUtil.TestSuite
124TestLoader = TestUtil.TestLoader124TestLoader = TestUtil.TestLoader
125125
126# Tests should run in a clean and clearly defined environment. The goal is to
127# keep them isolated from the running environment as mush as possible. The test
128# framework ensures the variables defined below are set (or deleted if the
129# value is None) before a test is run and reset to their original value after
130# the test is run. Generally if some code depends on an environment variable,
131# the tests should start without this variable in the environment. There are a
132# few exceptions but you shouldn't violate this rule lightly.
133isolated_environ = {
134 'BZR_HOME': None,
135 'HOME': None,
136 # bzr now uses the Win32 API and doesn't rely on APPDATA, but the
137 # tests do check our impls match APPDATA
138 'BZR_EDITOR': None, # test_msgeditor manipulates this variable
139 'VISUAL': None,
140 'EDITOR': None,
141 'BZR_EMAIL': None,
142 'BZREMAIL': None, # may still be present in the environment
143 'EMAIL': 'jrandom@example.com', # set EMAIL as bzr does not guess
144 'BZR_PROGRESS_BAR': None,
145 'BZR_LOG': None,
146 'BZR_PLUGIN_PATH': None,
147 'BZR_DISABLE_PLUGINS': None,
148 'BZR_PLUGINS_AT': None,
149 'BZR_CONCURRENCY': None,
150 # Make sure that any text ui tests are consistent regardless of
151 # the environment the test case is run in; you may want tests that
152 # test other combinations. 'dumb' is a reasonable guess for tests
153 # going to a pipe or a StringIO.
154 'TERM': 'dumb',
155 'LINES': '25',
156 'COLUMNS': '80',
157 'BZR_COLUMNS': '80',
158 # Disable SSH Agent
159 'SSH_AUTH_SOCK': None,
160 # Proxies
161 'http_proxy': None,
162 'HTTP_PROXY': None,
163 'https_proxy': None,
164 'HTTPS_PROXY': None,
165 'no_proxy': None,
166 'NO_PROXY': None,
167 'all_proxy': None,
168 'ALL_PROXY': None,
169 # Nobody cares about ftp_proxy, FTP_PROXY AFAIK. So far at
170 # least. If you do (care), please update this comment
171 # -- vila 20080401
172 'ftp_proxy': None,
173 'FTP_PROXY': None,
174 'BZR_REMOTE_PATH': None,
175 # Generally speaking, we don't want apport reporting on crashes in
176 # the test envirnoment unless we're specifically testing apport,
177 # so that it doesn't leak into the real system environment. We
178 # use an env var so it propagates to subprocesses.
179 'APPORT_DISABLE': '1',
180 }
181
182
183def override_os_environ(test, env=None):
184 """Modify os.environ keeping a copy.
185
186 :param test: A test instance
187
188 :param env: A dict containing variable definitions to be installed
189 """
190 if env is None:
191 env = isolated_environ
192 test._original_os_environ = dict([(var, value)
193 for var, value in os.environ.iteritems()])
194 for var, value in env.iteritems():
195 osutils.set_or_unset_env(var, value)
196 if var not in test._original_os_environ:
197 # The var is new, add it with a value of None, so
198 # restore_os_environ will delete it
199 test._original_os_environ[var] = None
200
201
202def restore_os_environ(test):
203 """Restore os.environ to its original state.
204
205 :param test: A test instance previously passed to override_os_environ.
206 """
207 for var, value in test._original_os_environ.iteritems():
208 # Restore the original value (or delete it if the value has been set to
209 # None in override_os_environ).
210 osutils.set_or_unset_env(var, value)
211
212
126class ExtendedTestResult(testtools.TextTestResult):213class ExtendedTestResult(testtools.TextTestResult):
127 """Accepts, reports and accumulates the results of running tests.214 """Accepts, reports and accumulates the results of running tests.
128215
@@ -794,6 +881,25 @@
794 return NullProgressView()881 return NullProgressView()
795882
796883
884def isolated_doctest_setUp(test):
885 override_os_environ(test)
886
887
888def isolated_doctest_tearDown(test):
889 restore_os_environ(test)
890
891
892def IsolatedDocTestSuite(*args, **kwargs):
893 """Overrides doctest.DocTestSuite to handle isolation.
894
895 The method is really a factory and users are expected to use it as such.
896 """
897
898 kwargs['setUp'] = isolated_doctest_setUp
899 kwargs['tearDown'] = isolated_doctest_tearDown
900 return doctest.DocTestSuite(*args, **kwargs)
901
902
797class TestCase(testtools.TestCase):903class TestCase(testtools.TestCase):
798 """Base class for bzr unit tests.904 """Base class for bzr unit tests.
799905
@@ -1538,55 +1644,7 @@
1538 return value1644 return value
15391645
1540 def _cleanEnvironment(self):1646 def _cleanEnvironment(self):
1541 new_env = {1647 for name, value in isolated_environ.iteritems():
1542 'BZR_HOME': None, # Don't inherit BZR_HOME to all the tests.
1543 'HOME': os.getcwd(),
1544 # bzr now uses the Win32 API and doesn't rely on APPDATA, but the
1545 # tests do check our impls match APPDATA
1546 'BZR_EDITOR': None, # test_msgeditor manipulates this variable
1547 'VISUAL': None,
1548 'EDITOR': None,
1549 'BZR_EMAIL': None,
1550 'BZREMAIL': None, # may still be present in the environment
1551 'EMAIL': 'jrandom@example.com', # set EMAIL as bzr does not guess
1552 'BZR_PROGRESS_BAR': None,
1553 'BZR_LOG': None,
1554 'BZR_PLUGIN_PATH': None,
1555 'BZR_DISABLE_PLUGINS': None,
1556 'BZR_PLUGINS_AT': None,
1557 'BZR_CONCURRENCY': None,
1558 # Make sure that any text ui tests are consistent regardless of
1559 # the environment the test case is run in; you may want tests that
1560 # test other combinations. 'dumb' is a reasonable guess for tests
1561 # going to a pipe or a StringIO.
1562 'TERM': 'dumb',
1563 'LINES': '25',
1564 'COLUMNS': '80',
1565 'BZR_COLUMNS': '80',
1566 # SSH Agent
1567 'SSH_AUTH_SOCK': None,
1568 # Proxies
1569 'http_proxy': None,
1570 'HTTP_PROXY': None,
1571 'https_proxy': None,
1572 'HTTPS_PROXY': None,
1573 'no_proxy': None,
1574 'NO_PROXY': None,
1575 'all_proxy': None,
1576 'ALL_PROXY': None,
1577 # Nobody cares about ftp_proxy, FTP_PROXY AFAIK. So far at
1578 # least. If you do (care), please update this comment
1579 # -- vila 20080401
1580 'ftp_proxy': None,
1581 'FTP_PROXY': None,
1582 'BZR_REMOTE_PATH': None,
1583 # Generally speaking, we don't want apport reporting on crashes in
1584 # the test envirnoment unless we're specifically testing apport,
1585 # so that it doesn't leak into the real system environment. We
1586 # use an env var so it propagates to subprocesses.
1587 'APPORT_DISABLE': '1',
1588 }
1589 for name, value in new_env.iteritems():
1590 self.overrideEnv(name, value)1648 self.overrideEnv(name, value)
15911649
1592 def _captureVar(self, name, newvalue):1650 def _captureVar(self, name, newvalue):
@@ -3836,13 +3894,7 @@
3836 return []3894 return []
3837 return [3895 return [
3838 'bzrlib',3896 'bzrlib',
3839 # FIXME: Fixing bug #690563 revealed an isolation problem in the single3897 'bzrlib.branchbuilder',
3840 # doctest for branchbuilder. Uncomment this when bug #321320 is fixed
3841 # to ensure the issue is addressed (note that to reproduce the bug in
3842 # the doctest below, one should comment the 'email' config var in
3843 # bazaar.conf (or anywhere else). This means an setup where *no* user
3844 # is being set at all in the environment.
3845# 'bzrlib.branchbuilder',
3846 'bzrlib.decorators',3898 'bzrlib.decorators',
3847 'bzrlib.export',3899 'bzrlib.export',
3848 'bzrlib.inventory',3900 'bzrlib.inventory',
@@ -3914,8 +3966,8 @@
3914 try:3966 try:
3915 # note that this really does mean "report only" -- doctest3967 # note that this really does mean "report only" -- doctest
3916 # still runs the rest of the examples3968 # still runs the rest of the examples
3917 doc_suite = doctest.DocTestSuite(mod,3969 doc_suite = IsolatedDocTestSuite(
3918 optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)3970 mod, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)
3919 except ValueError, e:3971 except ValueError, e:
3920 print '**failed to get doctest for: %s\n%s' % (mod, e)3972 print '**failed to get doctest for: %s\n%s' % (mod, e)
3921 raise3973 raise
39223974
=== modified file 'bzrlib/tests/test_selftest.py'
--- bzrlib/tests/test_selftest.py 2010-12-26 16:23:16 +0000
+++ bzrlib/tests/test_selftest.py 2011-01-07 11:18:35 +0000
@@ -17,7 +17,7 @@
17"""Tests for the test framework."""17"""Tests for the test framework."""
1818
19from cStringIO import StringIO19from cStringIO import StringIO
20from doctest import ELLIPSIS20import doctest
21import os21import os
22import signal22import signal
23import sys23import sys
@@ -92,7 +92,7 @@
92 "text", "plain", {"charset": "utf8"})))92 "text", "plain", {"charset": "utf8"})))
93 self.assertThat(u"".join(log.iter_text()), Equals(self.get_log()))93 self.assertThat(u"".join(log.iter_text()), Equals(self.get_log()))
94 self.assertThat(self.get_log(),94 self.assertThat(self.get_log(),
95 DocTestMatches(u"...a test message\n", ELLIPSIS))95 DocTestMatches(u"...a test message\n", doctest.ELLIPSIS))
9696
9797
98class TestUnicodeFilename(tests.TestCase):98class TestUnicodeFilename(tests.TestCase):
@@ -3210,7 +3210,8 @@
3210 tpr.register('bar', 'bBB.aAA.rRR')3210 tpr.register('bar', 'bBB.aAA.rRR')
3211 self.assertEquals('bbb.aaa.rrr', tpr.get('bar'))3211 self.assertEquals('bbb.aaa.rrr', tpr.get('bar'))
3212 self.assertThat(self.get_log(),3212 self.assertThat(self.get_log(),
3213 DocTestMatches("...bar...bbb.aaa.rrr...BB.aAA.rRR", ELLIPSIS))3213 DocTestMatches("...bar...bbb.aaa.rrr...BB.aAA.rRR",
3214 doctest.ELLIPSIS))
32143215
3215 def test_get_unknown_prefix(self):3216 def test_get_unknown_prefix(self):
3216 tpr = self._get_registry()3217 tpr = self._get_registry()
@@ -3453,3 +3454,121 @@
3453 self.fail(output.getvalue())3454 self.fail(output.getvalue())
3454 # We get our value back3455 # We get our value back
3455 self.assertEquals('42', os.environ.get('MYVAR'))3456 self.assertEquals('42', os.environ.get('MYVAR'))
3457
3458
3459class TestIsolatedEnv(tests.TestCase):
3460 """Test isolating tests from os.environ.
3461
3462 Since we use tests that are already isolated from os.environ a bit of care
3463 should be taken when designing the tests to avoid bootstrap side-effects.
3464 The tests start an already clean os.environ which allow doing valid
3465 assertions about which variables are present or not and design tests around
3466 these assertions.
3467 """
3468
3469 class ScratchMonkey(tests.TestCase):
3470
3471 def test_me(self):
3472 pass
3473
3474 def test_basics(self):
3475 # Make sure we know the definition of BZR_HOME: not part of os.environ
3476 # for tests.TestCase.
3477 self.assertTrue('BZR_HOME' in tests.isolated_environ)
3478 self.assertEquals(None, tests.isolated_environ['BZR_HOME'])
3479 # Being part of isolated_environ, BZR_HOME should not appear here
3480 self.assertFalse('BZR_HOME' in os.environ)
3481 # Make sure we know the definition of LINES: part of os.environ for
3482 # tests.TestCase
3483 self.assertTrue('LINES' in tests.isolated_environ)
3484 self.assertEquals('25', tests.isolated_environ['LINES'])
3485 self.assertEquals('25', os.environ['LINES'])
3486
3487 def test_injecting_unknown_variable(self):
3488 # BZR_HOME is known to be absent from os.environ
3489 test = self.ScratchMonkey('test_me')
3490 tests.override_os_environ(test, {'BZR_HOME': 'foo'})
3491 self.assertEquals('foo', os.environ['BZR_HOME'])
3492 tests.restore_os_environ(test)
3493 self.assertFalse('BZR_HOME' in os.environ)
3494
3495 def test_injecting_known_variable(self):
3496 test = self.ScratchMonkey('test_me')
3497 # LINES is known to be present in os.environ
3498 tests.override_os_environ(test, {'LINES': '42'})
3499 self.assertEquals('42', os.environ['LINES'])
3500 tests.restore_os_environ(test)
3501 self.assertEquals('25', os.environ['LINES'])
3502
3503 def test_deleting_variable(self):
3504 test = self.ScratchMonkey('test_me')
3505 # LINES is known to be present in os.environ
3506 tests.override_os_environ(test, {'LINES': None})
3507 self.assertTrue('LINES' not in os.environ)
3508 tests.restore_os_environ(test)
3509 self.assertEquals('25', os.environ['LINES'])
3510
3511
3512class TestDocTestSuiteIsolation(tests.TestCase):
3513 """Test that `tests.DocTestSuite` isolates doc tests from os.environ.
3514
3515 Since tests.TestCase alreay provides an isolation from os.environ, we use
3516 the clean environment as a base for testing. To precisely capture the
3517 isolation provided by tests.DocTestSuite, we use doctest.DocTestSuite to
3518 compare against.
3519
3520 We want to make sure `tests.DocTestSuite` respect `tests.isolated_environ`,
3521 not `os.environ` so each test overrides it to suit its needs.
3522
3523 """
3524
3525 def get_doctest_suite_for_string(self, klass, string):
3526 class Finder(doctest.DocTestFinder):
3527
3528 def find(*args, **kwargs):
3529 test = doctest.DocTestParser().get_doctest(
3530 string, {}, 'foo', 'foo.py', 0)
3531 return [test]
3532
3533 suite = klass(test_finder=Finder())
3534 return suite
3535
3536 def run_doctest_suite_for_string(self, klass, string):
3537 suite = self.get_doctest_suite_for_string(klass, string)
3538 output = StringIO()
3539 result = tests.TextTestResult(output, 0, 1)
3540 suite.run(result)
3541 return result, output
3542
3543 def assertDocTestStringSucceds(self, klass, string):
3544 result, output = self.run_doctest_suite_for_string(klass, string)
3545 if not result.wasStrictlySuccessful():
3546 self.fail(output.getvalue())
3547
3548 def assertDocTestStringFails(self, klass, string):
3549 result, output = self.run_doctest_suite_for_string(klass, string)
3550 if result.wasStrictlySuccessful():
3551 self.fail(output.getvalue())
3552
3553 def test_injected_variable(self):
3554 self.overrideAttr(tests, 'isolated_environ', {'LINES': '42'})
3555 test = """
3556 >>> import os
3557 >>> os.environ['LINES']
3558 '42'
3559 """
3560 # doctest.DocTestSuite fails as it sees '25'
3561 self.assertDocTestStringFails(doctest.DocTestSuite, test)
3562 # tests.DocTestSuite sees '42'
3563 self.assertDocTestStringSucceds(tests.IsolatedDocTestSuite, test)
3564
3565 def test_deleted_variable(self):
3566 self.overrideAttr(tests, 'isolated_environ', {'LINES': None})
3567 test = """
3568 >>> import os
3569 >>> os.environ.get('LINES')
3570 """
3571 # doctest.DocTestSuite fails as it sees '25'
3572 self.assertDocTestStringFails(doctest.DocTestSuite, test)
3573 # tests.DocTestSuite sees None
3574 self.assertDocTestStringSucceds(tests.IsolatedDocTestSuite, test)
34563575
=== modified file 'doc/developers/testing.txt'
--- doc/developers/testing.txt 2010-12-16 17:45:14 +0000
+++ doc/developers/testing.txt 2011-01-07 11:18:35 +0000
@@ -329,8 +329,11 @@
329We make selective use of doctests__. In general they should provide329We make selective use of doctests__. In general they should provide
330*examples* within the API documentation which can incidentally be tested. We330*examples* within the API documentation which can incidentally be tested. We
331don't try to test every important case using doctests |--| regular Python331don't try to test every important case using doctests |--| regular Python
332tests are generally a better solution. That is, we just use doctests to332tests are generally a better solution. That is, we just use doctests to make
333make our documentation testable, rather than as a way to make tests.333our documentation testable, rather than as a way to make tests. Be aware that
334doctests are not as well isolated as the unit tests, if you need more
335isolation, you're likely want to write unit tests anyway if only to get a
336better control of the test environment.
334337
335Most of these are in ``bzrlib/doc/api``. More additions are welcome.338Most of these are in ``bzrlib/doc/api``. More additions are welcome.
336339
337340
=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- doc/en/release-notes/bzr-2.3.txt 2010-12-24 16:30:36 +0000
+++ doc/en/release-notes/bzr-2.3.txt 2011-01-07 11:18:35 +0000
@@ -90,6 +90,13 @@
90 suite. This can include new facilities for writing tests, fixes to 90 suite. This can include new facilities for writing tests, fixes to
91 spurious test failures and changes to the way things should be tested.91 spurious test failures and changes to the way things should be tested.
9292
93* ``bzrlib.tests`` defines ``isolated_environ`` with the definitions of all
94 the environment variables the tests should care about. It also defines
95 ``override_os_environ`` and ``restore_os_environ`` to properly implement
96 isolation from ``os.environ`` for tests. ``bzrlib.tests`` now defines a
97 ``DocTestSuite`` class using this facility for all ``bzrlib``
98 doctests. (Vincent Ladeuil, #321320)
99
93* Catch exceptions related to bug #637821 during test cleanup to avoid100* Catch exceptions related to bug #637821 during test cleanup to avoid
94 spurious failures. (Vincent Ladeuil, #686008).101 spurious failures. (Vincent Ladeuil, #686008).
95102