##############################################################################
#
# Copyright (c) 2008 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
================
Basic Test Setup
================

``BasicTestSetup`` is a class to support easier setup of tests in Zope
3 projects. It acts merely as a container for shared functions,
methods and attributes needed by 'real' test setups which are derived
from it. Itself provides *no* ``getTestSuite()`` method, which is
needed to setup real tests.

A ``BasicTestSetup`` tries to find all doctest files defined in a given
package. See `functionalsetup.py` for setting up real functional tests
and `unittestsetup.py` for 'real' setup of unittests.

The TestSetup classes all search and handle doctest files.

All we need to setup a testsuite, is the package to search::

   >>> from z3c.testsetup.base import BasicTestSetup
   >>> basic_setup = BasicTestSetup('z3c.testsetup.tests.cave')
   >>> basic_setup
   <z3c.testsetup.base.BasicTestSetup object at 0x...>

Such we specified the package to search by a string in dotted name
notation. We can, however, also pass the loaded package as argument::

   >>> from z3c.testsetup.tests import cave
   >>> basic_setup = BasicTestSetup(cave)
   >>> basic_setup
   <z3c.testsetup.base.BasicTestSetup object at 0x...>

The package is stored as an instance-attribute::

   >>> basic_setup.package
   <module 'z3c.testsetup.tests.cave' from ...>

The ``BasicTestSetup`` serves merely as a container for attributes and
methods needed by derived classes, that provide proper test setup.

One of it's resposibilities is, to find doctest files, which is done
by the ``getDocTestFiles()`` method. If we run this method, we
get a list of filenames::

   >>> file_list = basic_setup.getDocTestFiles()
   >>> len(file_list)
   4

The filenames are all absolute::

   >>> import os.path
   >>> [x for x in file_list if not os.path.isabs(file_list[0])]
   []


Which files are found?
----------------------

By default, all .txt and .rst files are taken
into account, that do not start with a dot::

   >>> exts = ['.rst', '.txt']
   >>> [x for x in file_list if not os.path.splitext(x)[1].lower() in exts]
   []

All forms of an extension are found, regardless of whether they are
uppercase, lowercase or mixed-case::

   >>> file_list
   [...'...file2.TXT'...]

Also subdirectories are searched::

   >>> file_list
   [...'...subdirfile.txt'...]

Hidden directories and files, however, are skipped. To check this, we
look for a 'hidden' testfile put into a hidden directory in the `cave`
directory. We first make sure, that the hidden file really exists::

   >>> cavepath = os.path.dirname(cave.__file__)
   >>> hiddenpath = os.path.join(cavepath, '.hiddendir', 'hiddenfile.txt')
   >>> os.path.exists(hiddenpath)
   True

And now check that it was *not* included in the file list::

   >>> hiddenpath in file_list
   False

Also another hidden file, which resides in a regular (non-hidden)
directory was skipped:

   >>> hiddenfile = os.path.join(cavepath, '.hiddenfile.txt')
   >>> os.path.exists(hiddenfile)
   True

   >>> hiddenpath in file_list
   False

To provide a more finegrained filtering, ``BasicTestSetup`` provides a
method ``isTestFile(filepath)``, which returns ``True`` for accepted
files and ``False`` otherwise. This method is called for every file
found by ``getDoctesFiles``. By default it only filters files by their
filename extension and compares it with the instance-attribute
``extensions``, which by default is the list ``['.rst', '.txt']``::

   >>> basic_setup.extensions
   ['.rst', '.txt']

   >>> basic_setup.isTestFile('')
   False

   >>> basic_setup.isTestFile('cave.foo')
   False

Hidden files (i.e. such starting with a dot in filename) are ignored:

   >>> basic_setup.isTestFile('.hiddenfile.txt')
   False

How to find a customized set of files:
--------------------------------------

There are several possibilities to modify the search results of
``getDocTestFiles()``. If it is only a matter of filename extension,
the instance's attribute ``extensions`` can be modified::

   >>> basic_setup.extensions = ['.foo']
   >>> basic_setup.getDocTestFiles()
   ['...notatest1.foo']

If things need a more complex filtering, you can also redefine the
filter function, which by default is the ``isTestFile()`` function as
mentioned above.

You can pass an alternative filterfunction as keyword parameter
``filter_func`` to the ``BasicTestSetup`` constructor::

   >>> def myFilter(filename):
   ...     # We accept all '.txt' files, also hidden ones.
   ...     return filename.endswith('.txt')
   >>> basic_setup2 = BasicTestSetup(cave, filter_func=myFilter)
   >>> len(basic_setup2.getDocTestFiles())
   3

Note, that the filter function must accept a single parameter, which
should contain a filepath as string and it should return a boolean
value to indicate, whether the file given by the filepath should be
included in the test suite or not.

A different set of accepted filename extensions can also be passed to
the constructor, using the ``extensions`` keyword::

   >>> basic_setup3 = BasicTestSetup(cave, extensions=['.txt'])

Note, that the extensions should always be written with a leading dot
and in lower case. Such we can find only .txt files::

   >>> len(basic_setup3.getDocTestFiles())
   3

Now also the .TXT file was found, which was omitted in the test
before.

The set of directories, which are accepted as doctest containers, is
defined by the ``isTestDirectory`` method, which by default only skips
'hidden' directories, i.e. directories, that start with a dot ('.').

   >>> basic_setup3.isTestDirectory('foo/bar/somdir')
   True

   >>> basic_setup3.isTestDirectory('foo/bar/.hiddendir')
   False

You can change this behaviour by deriving your own setup class and
overwriting the method. This works also with derived classes like
``FunctionalTestSetup``.


Find terms in docfiles:
-----------------------

For convenience ``BasicTestSetup`` provides a method ``fileContains``,
which parses the contents of a file to match a list of regular
expressions. If every of the regular expressions in the list matched
at least one line of the file, ``True`` is returned, ``False``
otherwise.

``fileContains`` is a helper function to search files for certain
terms and expressions. It is implemented as a method (instead a
standalone function), to enable developers to replace it with a more
complex implementation that also accesses other instance attributes
like the package or similar.

File paths, that cannot be found, are treated as real errors as they signal an
error in the way we set everything up:

   >>> basic_setup4 = BasicTestSetup(cave, extensions=['.foo'])
   >>> basic_setup4.fileContains('blah')
   Traceback (most recent call last):
   ...
   IOError: [Errno 2] No such file or directory: 'blah'

We pick up an existing file path::

   >>> file_list = basic_setup4.getDocTestFiles()
   >>> len(file_list)
   1

   >>> filepath = file_list[0]
   >>> filepath.endswith('notatest1.foo')
   True

This file contains a string 'ME GROK SMASH ZCML!!', which we can
search for::

   >>> basic_setup4.regexp_list = ['ME GROK']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   True

   >>> basic_setup4.regexp_list = ['ME GROK IS DUMB']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   False

The terms to search are handled as real regular expressions as
provided by the ``re`` package::

   >>> basic_setup4.regexp_list = ['^ME (G|g)ROK.*']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   True

We can also search for several matches (which ALL have to be present):

   >>> basic_setup4.regexp_list = ['.*SMASH.*', '.*GROK.*']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   True

If one of the searched terms is not found, the whole thing fails::

   >>> basic_setup4.regexp_list = ['.*DUMB.*', '.*GROK.*']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   False

It does not matter, whether matches occur in the same line or in
different ones. In the example file there is also a heading stating
'This is not a test'. Let's check this::

   >>> basic_setup4.regexp_list = ['ME GROK', 'This is not']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   True


Note: The evaluation of regular expressions is done without any
modifiers. Namely the matching is case sensitive::

   >>> basic_setup4.regexp_list = ['me grok']
   >>> basic_setup4._regexs = None # Zap regex cache
   >>> basic_setup4.fileContains(filepath)
   False

Furthermore, matches are only done against one line at a time.



"""
