← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-answers-exception-modules into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-answers-exception-modules into launchpad:master.

Commit message:
lp.answers: Use IGNORE_EXCEPTION_MODULE_IN_PYTHON2

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/396285

This allows doctests that test tracebacks to work on both Python 2 and 3.  See https://pypi.org/project/zope.testing/#regular-expression-pattern-normalizing-output-checker for the semantics of this custom option flag.

There are lots of cases like this all over the codebase, but this branch only tackles lp.answers for now since the full patch for everything ends up being on the order of 10000 lines; if this looks OK then I expect to self-approve most of the rest to save on reviewer bandwidth, since it all follows pretty much the same pattern.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-answers-exception-modules into launchpad:master.
diff --git a/lib/lp/answers/doc/faq.txt b/lib/lp/answers/doc/faq.txt
index 5c8c479..594242d 100644
--- a/lib/lp/answers/doc/faq.txt
+++ b/lib/lp/answers/doc/faq.txt
@@ -320,17 +320,19 @@ That change is also considered an answer:
 It is not possible to modify the faq attribute directly:
 
     >>> fnord_question.faq = None
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    ForbiddenAttribute: ...
+    zope.security.interfaces.ForbiddenAttribute: ...
 
 And it is not allowed to call linkFAQ() when the FAQ is already linked:
 
     >>> message = fnord_question.linkFAQ(
     ...     no_priv, firefox_faq, 'See the FAQ.')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    FAQTargetError: Cannot call linkFAQ() with already linked FAQ.
+    lp.answers.errors.FAQTargetError: Cannot call linkFAQ() with already linked FAQ.
 
 A FAQ can be linked to a 'solved' question, in which case, the status is
 not changed.
diff --git a/lib/lp/answers/doc/faqtarget.txt b/lib/lp/answers/doc/faqtarget.txt
index 145cb91..0518e70 100644
--- a/lib/lp/answers/doc/faqtarget.txt
+++ b/lib/lp/answers/doc/faqtarget.txt
@@ -38,9 +38,10 @@ the target.
 
     >>> no_priv = getUtility(ILaunchBag).user
     >>> target.newFAQ(no_priv, 'Title', 'Summary', content='Content')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 In practice, this means that only the project's owner (aka maintainer)
 or one of its answer contacts is authorized to create a new FAQ.
diff --git a/lib/lp/answers/doc/questiontarget.txt b/lib/lp/answers/doc/questiontarget.txt
index a2879b9..99731b0 100644
--- a/lib/lp/answers/doc/questiontarget.txt
+++ b/lib/lp/answers/doc/questiontarget.txt
@@ -89,9 +89,10 @@ Anonymous users cannot use newQuestion().
     >>> login(ANONYMOUS)
     >>> question = target.newQuestion(
     ...     sample_person, 'This will fail', 'Failed?')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
 
 Retrieving questions
@@ -387,9 +388,10 @@ is only available to registered users.
 
     >>> name18 = getUtility(IPersonSet).getByName('name18')
     >>> target.addAnswerContact(name18, name18)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
 This method returns True when the contact was added the list and False when it
 was already on the list.
@@ -417,9 +419,10 @@ languages.
     >>> len(sample_person.languages)
     0
     >>> target.addAnswerContact(sample_person, sample_person)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    AddAnswerContactError: An answer contact must speak a language...
+    lp.answers.errors.AddAnswerContactError: An answer contact must speak a language...
 
 Answer contacts can be removed by using the removeAnswerContact() method.
 Like its counterpart, it returns True when the answer contact was removed and
@@ -438,9 +441,10 @@ Only registered users can remove an answer contact.
 
     >>> login(ANONYMOUS)
     >>> target.removeAnswerContact(name18, name18)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
 
 Supported languages
diff --git a/lib/lp/answers/doc/workflow.txt b/lib/lp/answers/doc/workflow.txt
index 4d2116e..c9d3c74 100644
--- a/lib/lp/answers/doc/workflow.txt
+++ b/lib/lp/answers/doc/workflow.txt
@@ -426,9 +426,10 @@ As a Launchpad administrator, so can Stub.
     >>> login(marilize.preferredemail.email)
     >>> spam_question.reject(
     ...     marilize, "We don't send free CDs any more.")
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 When rejecting a question, a comment explaining the reason is given.
 
@@ -484,9 +485,10 @@ It is not possible to change the status attribute directly.
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> question.status = QuestionStatus.INVALID
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    ForbiddenAttribute...
+    zope.security.interfaces.ForbiddenAttribute: ...
 
 A user having launchpad.Admin permission on the question can set the question
 status to an arbitrary value, by giving the new status and a comment
@@ -542,9 +544,10 @@ permission, an Unauthorized exception is thrown.
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> question.setStatus(sample_person, QuestionStatus.EXPIRED, 'Expire.')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
 
 Adding Comments Without Changing the Status
@@ -590,9 +593,10 @@ Users without launchpad.Moderator privileges cannot set the assignee.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> question.assignee = sample_person
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: (<lp.answers.model.question.Question ...>, 'assignee', 'launchpad.Append')
+    zope.security.interfaces.Unauthorized: (<lp.answers.model.question.Question ...>, 'assignee', 'launchpad.Append')
 
 
 Events
@@ -763,6 +767,7 @@ method.
 
     >>> login(stub.preferredemail.email)
     >>> question.setStatus(stub, QuestionStatus.OPEN, reject_message)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotMessageOwnerError...
+    lp.answers.errors.NotMessageOwnerError: ...
diff --git a/lib/lp/answers/stories/distribution-package-answer-contact.txt b/lib/lp/answers/stories/distribution-package-answer-contact.txt
index 45a8afb..048e495 100644
--- a/lib/lp/answers/stories/distribution-package-answer-contact.txt
+++ b/lib/lp/answers/stories/distribution-package-answer-contact.txt
@@ -36,9 +36,10 @@ To register themselves as answer contact, the user clicks on the
 'Set answer contact' link. They need to login to access that function.
 
     >>> anon_browser.getLink('Set answer contact').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open(
diff --git a/lib/lp/answers/stories/faq-add.txt b/lib/lp/answers/stories/faq-add.txt
index ca391a0..effa2fb 100644
--- a/lib/lp/answers/stories/faq-add.txt
+++ b/lib/lp/answers/stories/faq-add.txt
@@ -10,15 +10,17 @@ the project owner, therefore they cannot create a new FAQ.
     >>> user_browser.getLink('All FAQs').click()
 
     >>> user_browser.getLink('Create a new FAQ')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
     >>> user_browser.open(
     ...     'http://answers.launchpad.test/firefox/+createfaq')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 Sample Person does have that ability to create a FAQ because they are the
 project owner. They are looking for a FAQ about RSS, but they do not find
diff --git a/lib/lp/answers/stories/faq-browse-and-search.txt b/lib/lp/answers/stories/faq-browse-and-search.txt
index 8826620..2d3555f 100644
--- a/lib/lp/answers/stories/faq-browse-and-search.txt
+++ b/lib/lp/answers/stories/faq-browse-and-search.txt
@@ -178,11 +178,13 @@ distribution FAQs.
 Asking for a non-existent FAQ or an invalid ID will raise a 404 error.
 
     >>> anon_browser.open('http://answers.launchpad.test/ubuntu/+faq/171717')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
 
     >>> anon_browser.open('http://answers.launchpad.test/ubuntu/+faq/bad')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
diff --git a/lib/lp/answers/stories/faq-edit.txt b/lib/lp/answers/stories/faq-edit.txt
index 820efd7..dc5969c 100644
--- a/lib/lp/answers/stories/faq-edit.txt
+++ b/lib/lp/answers/stories/faq-edit.txt
@@ -13,28 +13,32 @@ appear for the anonymous user nor No Privileges Person:
     FAQ #7 : Questions : Mozilla Firefox
 
     >>> anon_browser.getLink('Edit FAQ')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
     ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
     >>> user_browser.open('http://answers.launchpad.test/firefox/+faq/7')
     >>> user_browser.getLink('Edit FAQ')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
     ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
 Even trying to access the link directly will fail:
 
     >>> anon_browser.open(
     ...     'http://answers.launchpad.test/firefox/+faq/7/+edit')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
     ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
     >>> user_browser.open(
     ...     'http://answers.launchpad.test/firefox/+faq/7/+edit')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
     ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 The link is accessible to Sample Person who is the owner of the Firefox
 project:
diff --git a/lib/lp/answers/stories/project-add-question.txt b/lib/lp/answers/stories/project-add-question.txt
index ecd8271..dba5767 100644
--- a/lib/lp/answers/stories/project-add-question.txt
+++ b/lib/lp/answers/stories/project-add-question.txt
@@ -26,9 +26,10 @@ Project in this case.
 
     >>> anon_browser.open('http://answers.launchpad.test/mozilla')
     >>> anon_browser.getLink('Ask a question').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
     >>> user_browser.open('http://answers.launchpad.test/mozilla')
     >>> user_browser.getLink('Ask a question').click()
diff --git a/lib/lp/answers/stories/question-add.txt b/lib/lp/answers/stories/question-add.txt
index b8cfea0..1b7ec27 100644
--- a/lib/lp/answers/stories/question-add.txt
+++ b/lib/lp/answers/stories/question-add.txt
@@ -18,9 +18,10 @@ The user sees an involvement link to ask a question.
 Asking a new question requires logging in:
 
     >>> browser.getLink('Ask a question').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
     >>> user_browser.open('http://answers.launchpad.test/ubuntu/')
     >>> user_browser.getLink('Ask a question').click()
     >>> print(user_browser.title)
diff --git a/lib/lp/answers/stories/question-browse-and-search.txt b/lib/lp/answers/stories/question-browse-and-search.txt
index 6d587ee..0abf0ed 100644
--- a/lib/lp/answers/stories/question-browse-and-search.txt
+++ b/lib/lp/answers/stories/question-browse-and-search.txt
@@ -41,9 +41,10 @@ For projects that don't have products, the Answers facet is disabled.
 
     >>> browser.open('http://launchpad.test/aaa')
     >>> browser.getLink('Answers')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
      ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
 == Browsing Questions ==
 
@@ -83,15 +84,17 @@ out.
     >>> 'Next' in browser.contents
     True
     >>> browser.getLink('Next')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ..
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
     >>> 'Last' in browser.contents
     True
     >>> browser.getLink('Last')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ..
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
 
 He decides to go the first page. He remembered one question title that
 might have been remotely related to his problem.
@@ -104,15 +107,17 @@ greyed out:
     >>> 'Previous' in browser.contents
     True
     >>> browser.getLink('Previous')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ..
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
     >>> 'First' in browser.contents
     True
     >>> browser.getLink('First')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ..
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
 
 When he passes the mouse over the question's row, the beginning of the
 description appears in a small pop-up:
@@ -370,9 +375,10 @@ They need to login to access that page:
     ...     'http://launchpad.test/ubuntu/+source/mozilla-firefox/'
     ...     '+questions')
     >>> anon_browser.getLink('My questions').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
     >>> sample_person_browser = setupBrowser(
     ...     auth='Basic test@xxxxxxxxxxxxx:test')
@@ -441,9 +447,10 @@ They need to login to access that page:
 
     >>> anon_browser.open('http://launchpad.test/distros/ubuntu/+questions')
     >>> anon_browser.getLink('Need attention').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
     >>> sample_person_browser.open(
     ...     'http://launchpad.test/distros/ubuntu/+questions')
diff --git a/lib/lp/answers/stories/question-edit.txt b/lib/lp/answers/stories/question-edit.txt
index 3f7a2bc..be19a95 100644
--- a/lib/lp/answers/stories/question-edit.txt
+++ b/lib/lp/answers/stories/question-edit.txt
@@ -7,9 +7,10 @@ title and description.
 
     >>> anon_browser.open('http://launchpad.test/firefox/+question/2')
     >>> anon_browser.getLink('Edit question').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
     ...
-    Unauthorized...
+    zope.security.interfaces.Unauthorized: ...
 
     >>> test_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> test_browser.open('http://launchpad.test/firefox/+question/2')
diff --git a/lib/lp/answers/stories/question-overview.txt b/lib/lp/answers/stories/question-overview.txt
index 0a22246..8639aa6 100644
--- a/lib/lp/answers/stories/question-overview.txt
+++ b/lib/lp/answers/stories/question-overview.txt
@@ -235,14 +235,16 @@ Asking for a non-existent question or an invalid ID will still raise a
 404 though:
 
     >>> browser.open('http://answers.launchpad.test/questions/255')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
 
     >>> browser.open('http://answers.launchpad.test/questions/bad_id')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
 
 Also If you access a question through the wrong context, you'll be
 redirected to the question in the proper context. (For example, this is
@@ -266,14 +268,16 @@ It also works with pages below that URL:
 But again, an invalid ID still raises a 404:
 
     >>> browser.open('http://answers.launchpad.test/ubuntu/+question/255')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
 
     >>> browser.open(
     ...     'http://answers.launchpad.test/ubuntu/+question/bad_id')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    NotFound: ...
+    zope.publisher.interfaces.NotFound: ...
 
 
diff --git a/lib/lp/answers/stories/question-reject-and-change-status.txt b/lib/lp/answers/stories/question-reject-and-change-status.txt
index 9b2c22b..44467a4 100644
--- a/lib/lp/answers/stories/question-reject-and-change-status.txt
+++ b/lib/lp/answers/stories/question-reject-and-change-status.txt
@@ -9,18 +9,20 @@ don't have access to that feature.
 
     >>> user_browser.open('http://launchpad.test/firefox/+question/2')
     >>> user_browser.getLink('Reject question')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
 
 Even when trying to access the page directly, they will get an unauthorized
 error.
 
     >>> user_browser.open(
     ...     'http://launchpad.test/firefox/+question/2/+reject')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 To reject the question, the user clicks on the 'Reject Question' link.
 
@@ -99,9 +101,10 @@ That action isn't available to a non-privileged user:
 
     >>> browser.open('http://launchpad.test/firefox/+question/2')
     >>> browser.getLink('Change status')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    LinkNotFoundError...
+    zope.testbrowser.browser.LinkNotFoundError
 
 The change status form is available to an administrator through the
 'Change status' link.
diff --git a/lib/lp/answers/stories/this-is-a-faq.txt b/lib/lp/answers/stories/this-is-a-faq.txt
index ff6e5d9..e1f4db8 100644
--- a/lib/lp/answers/stories/this-is-a-faq.txt
+++ b/lib/lp/answers/stories/this-is-a-faq.txt
@@ -206,21 +206,24 @@ Since No Privileges Person isn't an answer contact for the project nor
 the project owner, they don't have the possibility to create a new FAQ.
 
     >>> user_browser.getLink('Create a FAQ')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
     >>> user_browser.getLink('Link to a FAQ').click()
     >>> user_browser.getLink('create a new FAQ')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    LinkNotFoundError
+    zope.testbrowser.browser.LinkNotFoundError
 
     >>> user_browser.open(
     ...     'http://answers.launchpad.test/firefox/+question/2/+createfaq')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
-    Unauthorized: ...
+    zope.security.interfaces.Unauthorized: ...
 
 Sample Person who is the project owner does have that ability.
 
diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
index e620147..7cb4746 100644
--- a/lib/lp/testing/pages.py
+++ b/lib/lp/testing/pages.py
@@ -929,10 +929,8 @@ def PageTestSuite(storydir, package=None, setUp=setUpGlobs, **kw):
     suite = unittest.TestSuite()
     # Add tests to the suite individually.
     if filenames:
-        checker = doctest.OutputChecker()
         paths = [os.path.join(storydir, filename) for filename in filenames]
         suite.addTest(LayeredDocFileSuite(
-            paths=paths,
-            package=package, checker=checker, stdout_logging=False,
+            paths=paths, package=package, stdout_logging=False,
             layer=PageTestLayer, setUp=setUp, **kw))
     return suite
diff --git a/lib/lp/testing/systemdocs.py b/lib/lp/testing/systemdocs.py
index 706fa75..216ab84 100644
--- a/lib/lp/testing/systemdocs.py
+++ b/lib/lp/testing/systemdocs.py
@@ -29,6 +29,7 @@ import six
 import transaction
 from zope.component import getUtility
 from zope.testing.loggingsupport import Handler
+from zope.testing.renormalizing import OutputChecker
 
 from lp.services.config import config
 from lp.services.database.sqlbase import flush_database_updates
@@ -81,6 +82,9 @@ class FilePrefixStrippingDocTestParser(doctest.DocTestParser):
 default_parser = FilePrefixStrippingDocTestParser()
 
 
+default_checker = OutputChecker()
+
+
 class StdoutHandler(Handler):
     """A logging handler that prints log messages to sys.stdout.
 
@@ -147,6 +151,7 @@ def LayeredDocFileSuite(paths, id_extensions=None, **kw):
         id_extensions = []
     kw.setdefault('optionflags', default_optionflags)
     kw.setdefault('parser', default_parser)
+    kw.setdefault('checker', default_checker)
 
     # Make sure that paths are resolved relative to our caller
     kw['package'] = doctest._normalize_module(kw.get('package'))