[Checkins] SVN: manuel/branches/benji-add-test-cases/src/ checkpoint first cut
Benji York
benji at zope.com
Thu Jul 9 22:37:23 EDT 2009
Log message for revision 101782:
checkpoint first cut
Changed:
U manuel/branches/benji-add-test-cases/src/index.txt
U manuel/branches/benji-add-test-cases/src/manuel/README.txt
U manuel/branches/benji-add-test-cases/src/manuel/__init__.py
A manuel/branches/benji-add-test-cases/src/manuel/testcase.py
U manuel/branches/benji-add-test-cases/src/manuel/testing.py
U manuel/branches/benji-add-test-cases/src/manuel/tests.py
-=-
Modified: manuel/branches/benji-add-test-cases/src/index.txt
===================================================================
--- manuel/branches/benji-add-test-cases/src/index.txt 2009-07-10 02:37:05 UTC (rev 101781)
+++ manuel/branches/benji-add-test-cases/src/index.txt 2009-07-10 02:37:22 UTC (rev 101782)
@@ -20,7 +20,7 @@
def my_func(bar):
return foo.baz(bar)
-Incidentally, the implementation of :mod:`manuel.codeblock` is only 22 lines of
+Incidentally, the implementation of :mod:`manuel.codeblock` is only 23 lines of
code.
The plug-ins included in Manuel make good examples while being quite useful in
@@ -63,7 +63,11 @@
:ref:`manuel.isolation <isolation>`
makes it easier to have test isolation in doctests
+:ref:`manuel.testcase <testcase>`
+ identify parts of tests as individual test cases so they can be run
+ independently
+
.. reset-globs
.. _getting-started:
@@ -79,7 +83,8 @@
import manuel.doctest
import manuel.footnote
- m = manuel.doctest.Manuel() + manuel.footnote.Manuel()
+ m = manuel.doctest.Manuel()
+ m += manuel.footnote.Manuel()
You would then pass the Manuel instance to a :class:`manuel.testing.TestSuite`,
including the names of documents you want to process:
@@ -213,19 +218,26 @@
================
When writing documentation the need often arrises to describe the contents of
-files or other non-python information. You may also want to put that
+files or other non-Python information. You may also want to put that
information under test. :mod:`manuel.capture` helps with that.
-If you were writing the problems for a programming contest, you might want to
-describe the input and output files for each challenge.
+For example, if you were writing the problems for a programming contest, you
+might want to describe the input and output files for each challenge, but you
+want to be sure that your examples are correct.
-You would then show the contestant the expected output of their program. But
-you want to be sure that your examples are correct.
-
To do that you might write your document like this:
::
+ Challenge 1
+ ===========
+
+ Write a program that sorts the numbers in a file.
+
+
+ Example
+ -------
+
Given this example input file::
6
@@ -248,11 +260,11 @@
20
65
- .. -> expected
+ .. -> output
- >>> correct = '\n'.join(
- ... map(str, sorted(map(int, input.splitlines())))) + '\n'
- >>> expected == correct
+ >>> input_lines = input.splitlines()
+ >>> correct = '\n'.join(map(str, sorted(map(int, input_lines)))) + '\n'
+ >>> output == correct
True
.. -> source
@@ -276,9 +288,38 @@
Of course, lines that start with ".. " are reST comments, so when the document
is rendered with docutils or Sphinx, the tests will dissapear and only the
-intended document contents will remain.
+intended document contents will remain. Like so::
+ Challenge 1
+ ===========
+ Write a program that sorts the numbers in a file.
+
+
+ Example
+ -------
+
+ Given this example input file::
+
+ 6
+ 1
+ 8
+ 20
+ 11
+ 65
+ 2
+
+ Your program should generare this output file::
+
+ 1
+ 2
+ 6
+ 8
+ 11
+ 20
+ 65
+
+
.. reset-globs
.. _code-blocks:
@@ -492,7 +533,7 @@
>>> document.process_with(m, globs={})
>>> print document.formatted()
-It is possible to reference more than one footnote on a single line.
+It is also possible to reference more than one footnote on a single line.
::
@@ -740,6 +781,188 @@
>>> print document.formatted(),
+.. reset-globs
+.. _testcase:
+
+Identifying Test Cases
+======================
+
+If you want parts of a document to be individually accessible as test cases (to
+be able to run just a particular subset of them, for example), a parser can
+create a region that marks the beginning of a new test case.
+
+Two ways of identifying test cases are included in :mod:`manuel.testcase`:
+
+1. by section headings
+
+2. by explicit ".. test-case: NAME" markers.
+
+Grouping Tests by Heading
+-------------------------
+
+::
+
+ First Section
+ =============
+
+ Some prose.
+
+ >>> print 'first test case'
+
+ Some more prose.
+
+ >>> print 'still in the first test case'
+
+ Second Section
+ ==============
+
+ Even more prose.
+
+ >>> print 'second test case'
+
+.. -> source
+
+ >>> import manuel
+ >>> import manuel.testcase
+ >>> document = manuel.Document(source)
+ >>> m = manuel.testcase.SectionManuel()
+ >>> m += manuel.doctest.Manuel()
+ >>> document.process_with(m, globs={})
+ >>> print document.formatted(),
+ File "<memory>"...
+ Failed example:
+ print 'first test case'
+ Expected nothing
+ Got:
+ first test case
+ File "<memory>"...
+ Failed example:
+ print 'still in the first test case'
+ Expected nothing
+ Got:
+ still in the first test case
+ File "<memory>"...
+ Failed example:
+ print 'second test case'
+ Expected nothing
+ Got:
+ second test case
+
+.. now lets see if the regions are grouped as we expect
+
+ >>> import manuel.testing
+ >>> for regions in manuel.testing.group_regions_by_test_case(document):
+ ... print (regions.location, regions.id)
+ ('<memory>', None)
+ ('<memory>', 'First Section')
+ ('<memory>', 'Second Section')
+
+Given the above document, if you're using zope.testing's testrunner (located in bin/test), you could run just the tests in the second section with this command::
+
+ bin/test -t "file-name.txt:Second Section"
+
+Or, exploiting the fact that -t does a regex search (as opposed to a match)::
+
+ bin/test -t file-name.txt:Second
+
+
+Grouping Tests Explicitly
+-------------------------
+
+If you would like to identify test cases separately from sections, you can
+identify them with a marker::
+
+ First Section
+ =============
+
+ The following test will be in a test case that is not individually
+ identifiable.
+
+ >>> print 'first test case (unidentified)'
+
+ Some more prose.
+
+ .. test-case: first-named-test-case
+
+ >>> print 'first identified test case'
+
+
+ Second Section
+ ==============
+
+ The test case markers don't have to immediately proceed a test.
+
+ .. test-case: second-named-test-case
+
+ Even more prose.
+
+ >>> print 'second identified test case'
+
+.. -> source
+
+ >>> document = manuel.Document(source)
+ >>> m = manuel.testcase.MarkerManuel()
+ >>> m += manuel.doctest.Manuel()
+ >>> document.parse_with(m)
+ >>> for regions in manuel.testing.group_regions_by_test_case(document):
+ ... print regions.location, regions.id
+ <memory> None
+ <memory> first-named-test-case
+ <memory> second-named-test-case
+
+Again, given the above document and zope.testing, you could run just the second
+set of tests with this command::
+
+ bin/test -t file-name.txt:second-named-test-case
+
+Or, exploiting the fact that -t does a regex search again::
+
+ bin/test -t file-name.txt:second
+
+Even though the tests are individually accessable doesn't mean that they can't
+all be run at the same time::
+
+ bin/test -t file-name.txt
+
+Also, if you create a hierarchy of names, you can run groups of tests at a
+time. For example, lets say that you append "-important" to all your really
+important tests, you could then run the important tests for a single document
+like so::
+
+ bin/test -t 'file-name.txt:.*-important$'
+
+or all the "important" tests no matter what file they are in::
+
+ bin/test -t '-important$'
+
+Both Methods
+------------
+
+You can also combine more than one test case identification method if you want.
+Here's an example of building a Manuel stack that has doctests and both flavors
+of test case identification:
+
+.. code-block:: python
+
+ import manuel.doctest
+ import manuel.testcase
+
+ m = manuel.doctest.Manuel()
+ m += manuel.testcase.SectionManuel()
+ m += manuel.testcase.MarkerManuel()
+
+.. make sure above finds all the test cases appropriately
+
+ >>> document.parse_with(m)
+ >>> for regions in manuel.testing.group_regions_by_test_case(document):
+ ... print regions.location, regions.id
+ <memory> None
+ <memory> First Section
+ <memory> first-named-test-case
+ <memory> Second Section
+ <memory> second-named-test-case
+
+
Further Reading
===============
Modified: manuel/branches/benji-add-test-cases/src/manuel/README.txt
===================================================================
--- manuel/branches/benji-add-test-cases/src/manuel/README.txt 2009-07-10 02:37:05 UTC (rev 101781)
+++ manuel/branches/benji-add-test-cases/src/manuel/README.txt 2009-07-10 02:37:22 UTC (rev 101782)
@@ -555,3 +555,23 @@
Additional Information:
a = 1
b = 2
+
+
+Defining Test Cases
+===================
+
+If you want parts of a document to be accessable individually as test cases (to
+be able to run just a particular part of a document, for example), a parser can
+create a region that marks the beginning of a new test case.
+
+.. code-block:: python
+
+ new_test_case_regex = re.compile(r'^.. new-test-case: \w+', re.MULTILINE)
+
+ def parse(document):
+ for region in document.find_regions(new_test_case_regex):
+ document.claim_region(region)
+ id = region.start_match.group(1)
+ region.parsed = manuel.testing.TestCaseMarker(id)
+
+XXX finish this section
Modified: manuel/branches/benji-add-test-cases/src/manuel/__init__.py
===================================================================
--- manuel/branches/benji-add-test-cases/src/manuel/__init__.py 2009-07-10 02:37:05 UTC (rev 101781)
+++ manuel/branches/benji-add-test-cases/src/manuel/__init__.py 2009-07-10 02:37:22 UTC (rev 101782)
@@ -129,13 +129,64 @@
end += 1
return end
-class Document(object):
- def __init__(self, source, location='<memory>'):
+class RegionContainer(object):
+
+ location = '<memory>'
+ id = None
+
+ def __init__(self):
+ self.regions = []
+
+ def parse_with(self, m):
+ for parser in sort_handlers(m.parsers):
+ parser(self)
+
+ def evaluate_with(self, m, globs):
+ globs = GlobWrapper(globs)
+ for region in list(self):
+ for evaluater in sort_handlers(m.evaluaters):
+ evaluater(region, self, globs)
+
+ def format_with(self, m):
+ for formatter in sort_handlers(m.formatters):
+ formatter(self)
+
+ def process_with(self, m, globs):
+ """Run all phases of document processing using a Manuel instance.
+ """
+ self.parse_with(m)
+ self.evaluate_with(m, globs)
+ self.format_with(m)
+
+ def formatted(self):
+ """Return a string of all non-boolean-false formatted regions.
+ """
+ return ''.join(region.formatted for region in self if region.formatted)
+
+ def append(self, region):
+ self.regions.append(region)
+
+ def __iter__(self):
+ """Iterate over all regions of the document.
+ """
+ return iter(self.regions)
+
+ def __nonzero__(self):
+ return bool(self.regions)
+
+
+class Document(RegionContainer):
+
+ def __init__(self, source, location=None):
+ RegionContainer.__init__(self)
+
+ if location is not None:
+ self.location = location
+
self.source = newlineify(source)
- self.location = location
- self.regions = [Region(lineno=1, source=source)]
+ self.append(Region(lineno=1, source=source))
self.shadow_regions = []
def find_regions(self, start, end=None):
@@ -255,38 +306,7 @@
def insert_region_after(self, marker_region, new_region):
self.insert_region('after', marker_region, new_region)
- def parse_with(self, m):
- for parser in sort_handlers(m.parsers):
- parser(self)
- def evaluate_with(self, m, globs):
- globs = GlobWrapper(globs)
- for region in list(self):
- for evaluater in sort_handlers(m.evaluaters):
- evaluater(region, self, globs)
-
- def format_with(self, m):
- for formatter in sort_handlers(m.formatters):
- formatter(self)
-
- def process_with(self, m, globs):
- """Run all phases of document processing using a Manuel instance.
- """
- self.parse_with(m)
- self.evaluate_with(m, globs)
- self.format_with(m)
-
- def formatted(self):
- """Return a string of all non-boolean-false formatted regions.
- """
- return ''.join(region.formatted for region in self if region.formatted)
-
- def __iter__(self):
- """Iterate over all regions of the document.
- """
- return iter(self.regions)
-
-
class Manuel(object):
def __init__(self, parsers=None, evaluaters=None, formatters=None):
Added: manuel/branches/benji-add-test-cases/src/manuel/testcase.py
===================================================================
--- manuel/branches/benji-add-test-cases/src/manuel/testcase.py (rev 0)
+++ manuel/branches/benji-add-test-cases/src/manuel/testcase.py 2009-07-10 02:37:22 UTC (rev 101782)
@@ -0,0 +1,45 @@
+import manuel
+import manuel.testing
+import re
+import string
+import textwrap
+
+punctuation = re.escape(string.punctuation)
+SECTION_TITLE = re.compile(r'^.+$', re.MULTILINE)
+SECTION_UNDERLINE = re.compile('^[' + punctuation + ']+\s*$', re.MULTILINE)
+MARKER = re.compile(r'^.. test-case: (\S+)', re.MULTILINE)
+
+def find_section_headers(document):
+ for region in document.find_regions(SECTION_TITLE, SECTION_UNDERLINE):
+ # regions that represent titles will have two lines
+ if region.source.count('\n') != 2:
+ continue
+
+ title, underline = region.source.splitlines()
+
+ # the underline has to be the same length as or longer than the title
+ if len(underline) < len(title):
+ continue
+
+ # ok, this is a region we want
+ document.claim_region(region)
+
+ test_case_name = title.strip()
+ region.parsed = manuel.testing.TestCaseMarker(test_case_name)
+
+
+def find_markers(document):
+ for region in document.find_regions(MARKER):
+ document.claim_region(region)
+ test_case_name = region.start_match.group(1)
+ region.parsed = manuel.testing.TestCaseMarker(test_case_name)
+
+
+class SectionManuel(manuel.Manuel):
+ def __init__(self):
+ manuel.Manuel.__init__(self, [find_section_headers])
+
+
+class MarkerManuel(manuel.Manuel):
+ def __init__(self):
+ manuel.Manuel.__init__(self, [find_markers])
Property changes on: manuel/branches/benji-add-test-cases/src/manuel/testcase.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Modified: manuel/branches/benji-add-test-cases/src/manuel/testing.py
===================================================================
--- manuel/branches/benji-add-test-cases/src/manuel/testing.py 2009-07-10 02:37:05 UTC (rev 101781)
+++ manuel/branches/benji-add-test-cases/src/manuel/testing.py 2009-07-10 02:37:22 UTC (rev 101782)
@@ -1,3 +1,4 @@
+import itertools
import manuel
import os.path
import unittest
@@ -7,23 +8,22 @@
__all__ = ['TestSuite']
+class TestCaseMarker(object):
+
+ def __init__(self, id=''):
+ self.id = id
+
+
class TestCase(unittest.TestCase):
- def __init__(self, m, document, setUp=None, tearDown=None, globs=None):
+ def __init__(self, m, regions, globs, setUp=None, tearDown=None):
unittest.TestCase.__init__(self)
self.manuel = m
- self.document = document
+ self.regions = regions
+ self.globs = globs
self.setUp_func = setUp
self.tearDown_func = tearDown
- if globs is None:
- self.globs = {}
- else:
- self.globs = globs
- # we want to go ahead and do the parse phase so the countTestCases
- # method can get a good idea of how many tests there are
- document.parse_with(m)
-
def setUp(self):
if self.setUp_func is not None:
self.setUp_func(self)
@@ -33,9 +33,9 @@
self.tearDown_func(self)
def runTest(self):
- self.document.evaluate_with(self.manuel, self.globs)
- self.document.format_with(self.manuel)
- results = [r.formatted for r in self.document if r.formatted]
+ self.regions.evaluate_with(self.manuel, self.globs)
+ self.regions.format_with(self.manuel)
+ results = [r.formatted for r in self.regions if r.formatted]
if results:
DIVIDER = '-'*70 + '\n'
raise doctest.DocTestFailureException(
@@ -44,21 +44,65 @@
def debug(self):
self.setUp()
self.manuel.debug = True
- self.document.evaluate_with(self.manuel, self.globs)
+ self.regions.evaluate_with(self.manuel, self.globs)
self.tearDown()
def countTestCases(self):
- return len([r for r in self.document if r.parsed])
+ return len([r for r in self.regions if r.parsed])
def id(self):
- return self.document.location
+ return self.regions.id
def shortDescription(self):
- return "Manuel Test: " + self.document.location
+ if self.regions.id:
+ return self.regions.location + ':' + self.regions.id
+ else:
+ return self.regions.location
__str__ = __repr__ = shortDescription
+def group_regions_by_test_case(document):
+ """Generate groups of regions according to which testcase they belong"""
+ document_iter = iter(document)
+ marker = None
+ while True:
+ accumulated_regions = manuel.RegionContainer()
+ while True:
+ region = None # being defensive
+ try:
+ region = next(document_iter)
+ except StopIteration:
+ if not accumulated_regions:
+ break
+ else:
+ accumulated_regions.append(region)
+
+ if not isinstance(region.parsed, TestCaseMarker):
+ continue
+
+ # we just found a test case marker or hit the end of the
+ # document
+
+ # figure out what this test case's ID is
+ accumulated_regions.location = document.location
+ if marker is not None and marker.parsed.id:
+ accumulated_regions.id = marker.parsed.id
+
+ yield accumulated_regions
+ marker = region
+ break
+
+ # if there are no more regions, stop
+ try:
+ region = next(document_iter)
+ except StopIteration:
+ break
+
+ # put the region we peeked at back so the inner loop can consume it
+ document_iter = itertools.chain([region], document_iter)
+
+
def TestSuite(m, *paths, **kws):
"""A unittest suite that processes files with Manuel
@@ -83,6 +127,7 @@
"""
suite = unittest.TestSuite()
+ globs = kws.pop('globs', {})
# walk up the stack frame to find the module that called this function
for depth in range(2, 5):
@@ -95,10 +140,15 @@
for path in paths:
if os.path.isabs(path):
- abs_path = path
+ abs_path = os.path.normpath(path)
else:
- abs_path = doctest._module_relative_path(calling_module, path)
+ abs_path = os.path.abspath(
+ doctest._module_relative_path(calling_module, path))
+
document = manuel.Document(open(abs_path).read(), location=abs_path)
- suite.addTest(TestCase(m, document, **kws))
+ document.parse_with(m)
+ for regions in group_regions_by_test_case(document):
+ suite.addTest(TestCase(m, regions, globs, **kws))
+
return suite
Modified: manuel/branches/benji-add-test-cases/src/manuel/tests.py
===================================================================
--- manuel/branches/benji-add-test-cases/src/manuel/tests.py 2009-07-10 02:37:05 UTC (rev 101781)
+++ manuel/branches/benji-add-test-cases/src/manuel/tests.py 2009-07-10 02:37:22 UTC (rev 101782)
@@ -4,6 +4,7 @@
import manuel.codeblock
import manuel.doctest
import manuel.ignore
+import manuel.testcase
import manuel.testing
import os.path
import re
@@ -32,8 +33,6 @@
tests = ['../index.txt', 'table-example.txt', 'README.txt', 'bugs.txt']
- tests = map(get_abs_path, tests)
-
m = manuel.ignore.Manuel()
m += manuel.doctest.Manuel(optionflags=optionflags, checker=checker)
m += manuel.codeblock.Manuel()
More information about the Checkins
mailing list