[apycotlib] move checkers/preprocessors to have same organisation in dev and packages
authorPaul Tonelli <paul.tonelli@logilab.fr>
Tue, 08 Apr 2014 15:28:12 +0200
changeset 1468 3644b206edb5
parent 1467 d332fd51803d
child 1469 8e03b378a932
[apycotlib] move checkers/preprocessors to have same organisation in dev and packages various necessary changes: - path updates - import cubes.apycot in tests to be sure _apycotlib is added to path - rename pylint checkers to pycheckers to avoid confusion with package checkers - fix js path in jslint.py why: If you install a pkg, the checkers are installed in:: /usr/share/narval/checkers/apycot/* and /usr/share/narval is added to the path when launching them. In a dev environment, they were previously in :: _apycotlib/checkers/* This required specific paths to make a dev environment work (added in __init__.py). The new organisation puts checkers in:: _apycotlib/checkers/apycot/* The new organisation only requires to add _apycotlib to path to make checkers work. Same thing was done for preprpocessors.
__init__.py
__pkginfo__.py
_apycotlib/checkers/__init__.py
_apycotlib/checkers/apycot/__init__.py
_apycotlib/checkers/apycot/debian.py
_apycotlib/checkers/apycot/jslint.py
_apycotlib/checkers/apycot/python.py
_apycotlib/checkers/apycot/scenarios.py
_apycotlib/checkers/debian.py
_apycotlib/checkers/jslint.py
_apycotlib/checkers/python.py
_apycotlib/checkers/scenarios.py
_apycotlib/preprocessors/__init__.py
_apycotlib/preprocessors/apycot/__init__.py
_apycotlib/preprocessors/apycot/distutils.py
_apycotlib/preprocessors/distutils.py
debian/narval-apycot.install
debian/rules
test/unittest_checkers_jslint.py
test/unittest_checkers_pyunit.py
test/unittest_parser.py
--- a/__init__.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/__init__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -11,9 +11,6 @@
 else:
     import sys
     sys.modules['apycotlib'] = _apycotlib
-    import _apycotlib.checkers
-    sys.modules['checkers'] = _apycotlib.checkers
-    sys.modules['checkers.apycot'] = _apycotlib.checkers
-    import _apycotlib.preprocessors
-    sys.modules['preprocessors'] = _apycotlib.preprocessors
-    sys.modules['preprocessors.apycot'] = _apycotlib.preprocessors
+    from os.path import dirname, join
+    sys.path.append(join(dirname(__file__),'_apycotlib'))
+
--- a/__pkginfo__.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/__pkginfo__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -58,9 +58,9 @@
 
 if isdir('_apycotlib'): # test REQUIRED (to beimportable from everywhere)
     data_files.append([join('share', 'narval', 'checkers', modname),
-                       listdir(join('_apycotlib', 'checkers'))])
+                       listdir(join('_apycotlib', 'checkers', modname))])
     data_files.append([join('share', 'narval', 'preprocessors', modname),
-                       listdir(join('_apycotlib', 'preprocessors'))])
+                       listdir(join('_apycotlib', 'preprocessors', modname))])
 if isdir('ext'): # test REQUIRED (to beimportable from everywhere)
     data_files.append([join('share', 'narval', 'data', modname),
                        listdir(join('ext',))])
--- a/_apycotlib/checkers/__init__.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/_apycotlib/checkers/__init__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -1,133 +0,0 @@
-"""subpackage containing base checkers (mostly for python code and packaging
-standard used at Logilab)
-"""
-
-__docformat__ = "restructuredtext en"
-
-from os import walk
-from os.path import splitext, split, join
-
-from logilab.common.textutils import splitstrip
-from logilab.common.proc import RESOURCE_LIMIT_EXCEPTION
-
-from apycotlib import SUCCESS, NODATA, ERROR, TestStatus, ApycotObject
-
-class BaseChecker(ApycotObject):
-    id = None
-    __type__ = 'checker'
-    need_preprocessor = None
-
-    _best_status = None
-
-    def check(self, test):
-        self.status = None
-        try:
-            setup_status = self.setup_check(test)
-            self.set_status(setup_status)
-            if setup_status is None or setup_status:
-                self.set_status(self.do_check(test))
-                self.version_info()
-        finally:
-            self.set_status(self.tear_down_check(test))
-        # do it last to let checker do whatever they want to do.
-        new_status = self.merge_status(self.status, self.best_status)
-        if new_status is not self.status:
-            self.writer.info("Configuration's setting downgrade %s checker status '\
-                        'from <%s> to <%s>" , self.id, self.status, new_status)
-            self.set_status(new_status)
-        return self.status
-
-    def _get_best_status(self):
-        best_status = self._best_status
-        if best_status is None:
-            return None
-        if not isinstance(best_status, TestStatus):
-            best_status = TestStatus.get(best_status)
-        return best_status
-
-    def _set_best_status(self, value):
-        if not isinstance(value, TestStatus):
-            value = TestStatus.get(value)
-        self._best_status = value
-
-    best_status = property(_get_best_status, _set_best_status)
-
-    def version_info(self):
-        """hook for checkers to add their version information"""
-
-    def do_check(self, test):
-        """actually check the test"""
-        raise NotImplementedError("%s must defines a do_check method" % self.__class__)
-
-    def setup_check(self, test):
-        pass
-
-    def tear_down_check(self, test):
-        pass
-
-
-class AbstractFilteredFileChecker(BaseChecker):
-    """check a directory file by file, with an extension filter
-    """
-    checked_extensions =  None
-    options_def = {
-        'ignore': {
-            'type': 'csv', 'default': ['CVS', '.hg', '.git','.svn'],
-            'help': 'comma separated list of files or directories to ignore',
-            },
-        }
-
-    def filename_filter(self, dirpath, dirnames, filenames):
-        """Prune unwanted directories from dirnames (in place) and remove
-        unwanted files from filenames (inplace). The dirpath argument is provided to
-        enable more complex dirname/filename matching.
-        """
-        for dirname in dirnames[:]:
-            if self.ignored and join(dirpath,dirname).endswith(tuple(self.ignored)):
-                dirnames.remove(dirname)
-        for filename in filenames[:]:
-            if not ((self.extensions is None or
-                     filename.endswith(tuple(self.extensions))) and not
-                     join(dirpath, filename).endswith(tuple(self.ignored))):
-                filenames.remove(filename)
-
-    def __init__(self, writer, options=None, extensions=None):
-        super(AbstractFilteredFileChecker, self).__init__(writer, options)
-        self.extensions = extensions or self.checked_extensions
-        if isinstance(self.extensions, basestring):
-            self.extensions = (self.extensions,)
-        self._res = None
-        self._safe_dir = set()
-
-    def files_root(self, test):
-        return test.project_path(subpath=True)
-
-    def do_check(self, test):
-        """run the checker against <path> (usually a directory)
-
-        return true if the test succeeded, else false.
-        """
-        self.set_status(SUCCESS)
-        self._nbanalyzed = 0
-        self.ignored = self.options.get('ignore')
-        files_root = self.files_root(test)
-        self.writer.raw('file root', files_root)
-        for dirpath, dirnames, filenames in walk(self.files_root(test)):
-            #inplace pruning of dirnames and filenames
-            self.filename_filter(dirpath, dirnames, filenames)
-            for filename in filenames:
-                try:
-                    self.set_status(self.check_file(join(dirpath, filename)))
-                except RESOURCE_LIMIT_EXCEPTION:
-                    raise
-                except Exception, ex:
-                    self.writer.fatal(u"%s", ex, path=filename, tb=True)
-                    self.set_status(ERROR)
-                self._nbanalyzed += 1
-        self.writer.raw('total files analyzed', self._nbanalyzed)
-        if self._nbanalyzed <= 0:
-            self.set_status(NODATA)
-        return self.status
-
-    def check_file(self, path):
-        raise NotImplementedError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/checkers/apycot/__init__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,133 @@
+"""subpackage containing base checkers (mostly for python code and packaging
+standard used at Logilab)
+"""
+
+__docformat__ = "restructuredtext en"
+
+from os import walk
+from os.path import splitext, split, join
+
+from logilab.common.textutils import splitstrip
+from logilab.common.proc import RESOURCE_LIMIT_EXCEPTION
+
+from apycotlib import SUCCESS, NODATA, ERROR, TestStatus, ApycotObject
+
+class BaseChecker(ApycotObject):
+    id = None
+    __type__ = 'checker'
+    need_preprocessor = None
+
+    _best_status = None
+
+    def check(self, test):
+        self.status = None
+        try:
+            setup_status = self.setup_check(test)
+            self.set_status(setup_status)
+            if setup_status is None or setup_status:
+                self.set_status(self.do_check(test))
+                self.version_info()
+        finally:
+            self.set_status(self.tear_down_check(test))
+        # do it last to let checker do whatever they want to do.
+        new_status = self.merge_status(self.status, self.best_status)
+        if new_status is not self.status:
+            self.writer.info("Configuration's setting downgrade %s checker status '\
+                        'from <%s> to <%s>" , self.id, self.status, new_status)
+            self.set_status(new_status)
+        return self.status
+
+    def _get_best_status(self):
+        best_status = self._best_status
+        if best_status is None:
+            return None
+        if not isinstance(best_status, TestStatus):
+            best_status = TestStatus.get(best_status)
+        return best_status
+
+    def _set_best_status(self, value):
+        if not isinstance(value, TestStatus):
+            value = TestStatus.get(value)
+        self._best_status = value
+
+    best_status = property(_get_best_status, _set_best_status)
+
+    def version_info(self):
+        """hook for checkers to add their version information"""
+
+    def do_check(self, test):
+        """actually check the test"""
+        raise NotImplementedError("%s must defines a do_check method" % self.__class__)
+
+    def setup_check(self, test):
+        pass
+
+    def tear_down_check(self, test):
+        pass
+
+
+class AbstractFilteredFileChecker(BaseChecker):
+    """check a directory file by file, with an extension filter
+    """
+    checked_extensions =  None
+    options_def = {
+        'ignore': {
+            'type': 'csv', 'default': ['CVS', '.hg', '.git','.svn'],
+            'help': 'comma separated list of files or directories to ignore',
+            },
+        }
+
+    def filename_filter(self, dirpath, dirnames, filenames):
+        """Prune unwanted directories from dirnames (in place) and remove
+        unwanted files from filenames (inplace). The dirpath argument is provided to
+        enable more complex dirname/filename matching.
+        """
+        for dirname in dirnames[:]:
+            if self.ignored and join(dirpath,dirname).endswith(tuple(self.ignored)):
+                dirnames.remove(dirname)
+        for filename in filenames[:]:
+            if not ((self.extensions is None or
+                     filename.endswith(tuple(self.extensions))) and not
+                     join(dirpath, filename).endswith(tuple(self.ignored))):
+                filenames.remove(filename)
+
+    def __init__(self, writer, options=None, extensions=None):
+        super(AbstractFilteredFileChecker, self).__init__(writer, options)
+        self.extensions = extensions or self.checked_extensions
+        if isinstance(self.extensions, basestring):
+            self.extensions = (self.extensions,)
+        self._res = None
+        self._safe_dir = set()
+
+    def files_root(self, test):
+        return test.project_path(subpath=True)
+
+    def do_check(self, test):
+        """run the checker against <path> (usually a directory)
+
+        return true if the test succeeded, else false.
+        """
+        self.set_status(SUCCESS)
+        self._nbanalyzed = 0
+        self.ignored = self.options.get('ignore')
+        files_root = self.files_root(test)
+        self.writer.raw('file root', files_root)
+        for dirpath, dirnames, filenames in walk(self.files_root(test)):
+            #inplace pruning of dirnames and filenames
+            self.filename_filter(dirpath, dirnames, filenames)
+            for filename in filenames:
+                try:
+                    self.set_status(self.check_file(join(dirpath, filename)))
+                except RESOURCE_LIMIT_EXCEPTION:
+                    raise
+                except Exception, ex:
+                    self.writer.fatal(u"%s", ex, path=filename, tb=True)
+                    self.set_status(ERROR)
+                self._nbanalyzed += 1
+        self.writer.raw('total files analyzed', self._nbanalyzed)
+        if self._nbanalyzed <= 0:
+            self.set_status(NODATA)
+        return self.status
+
+    def check_file(self, path):
+        raise NotImplementedError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/checkers/apycot/debian.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,68 @@
+import apycotlib
+import logging
+import os
+import subprocess
+from checkers.apycot import BaseChecker
+
+def find_with_extension(path, extension):
+    path = os.path.expanduser(path)
+    for folder, subfolders, files in os.walk(path):
+        for filename in files:
+            if filename.endswith(extension):
+                yield os.path.join(folder, filename)
+
+class DebianLintianChecker(BaseChecker):
+    """Runs the lintian program after building a debian package. Lintian
+    checks for bugs and debian policy violations."""
+
+    id = 'lintian'
+
+    checked_extensions = ('.changes',)
+    options_def = {
+        'changes-files': {
+            'type': 'csv',
+            'required': False,
+            'help': 'changes files to check',
+        },
+    }
+
+    def get_output(self, path):
+        cmd = subprocess.Popen(['lintian', '-I', '--suppress-tags',
+                                'bad-distribution-in-changes-file', path],
+                               stdout=subprocess.PIPE, stdin=open('/dev/null'),
+                               stderr=subprocess.STDOUT)
+        for line in cmd.stdout:
+            yield line
+        cmd.wait()
+
+    def do_check(self, test):
+        status = apycotlib.SUCCESS
+        build_folder = os.path.join(test.project_path(), os.pardir)
+        change_files = find_with_extension(build_folder, '.changes')
+        if not change_files:
+            status = apycotlib.NODATA
+        for f in change_files:
+            iter_line = self.get_output(f)
+            for line in iter_line:
+                line_parts = line.split(':', 1)
+                try:
+                    mtype, msg = line_parts
+                except ValueError:
+                    self.writer.fatal('unexpected line %r' % line, path=f)
+                    for line in iter_line:
+                        self.writer.info('followed by: %r' % line, path=f)
+                    return apycotlib.ERROR
+                else:
+                    if mtype == 'I':
+                        self.writer.info(msg, path=f)
+                    elif mtype == 'W':
+                        self.writer.warning(msg, path=f)
+                    elif mtype == 'E':
+                        self.writer.error(msg, path=f)
+                        status = apycotlib.FAILURE
+                    else:
+                        self.writer.info(msg, path=f)
+        return status
+
+apycotlib.register('checker', DebianLintianChecker)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/checkers/apycot/jslint.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,178 @@
+import re
+import os
+import logging
+from subprocess import call
+from os.path import exists, dirname, join, abspath
+from re import compile
+
+from apycotlib import register, OutputParser, ParsedCommand, FAILURE
+from checkers.apycot import AbstractFilteredFileChecker
+
+# in source installation, jslint.js is in apycot/ext, we're currently in
+# apycot/_apycotlib/checkers/apycot
+JSLINT_PATH = join(dirname(dirname(dirname(dirname(abspath(__file__))))), 'ext', 'jslint.js')
+if not exists(JSLINT_PATH):
+    JSLINT_PATH = '/usr/share/narval/data/apycot/jslint.js'
+
+class JsLintParser(OutputParser):
+    """Simple Parser class interpretting
+
+    Lint at line 8 character 1: 'CubicWeb' is not defined.
+    CubicWeb.require('htmlhelpers.js');
+    """
+    non_zero_status_code = FAILURE
+
+    RE_MSG = re.compile(r'^Lint at line (\d+) character (\d+):\s*(.*)$')
+    #RE_NO_ISSUE = re.compile(r'^jslint: No problems found in ')
+
+    JSLINT_MSG = (
+        (logging.INFO,    "Unnecessary semicolon.",),
+        (logging.INFO,    "Unnecessary escapement.",),
+        (logging.INFO,    "The 'language' attribute is deprecated.",),
+        (logging.INFO,    "Inner functions should be listed at the top of the outer function.",),
+        (logging.INFO,    compile(r"Don't use extra leading zeros '.*?'\."),),
+        (logging.INFO,    "Confusing plusses.",),
+        (logging.INFO,    "Confusing minusses.",),
+        (logging.INFO,    compile(r"A trailing decimal point can be confused with a dot '.*?'i\."),),
+        (logging.INFO,    compile(r"A leading decimal point can be confused with a dot '\..*?'\."),),
+        (logging.INFO,    compile(r"'.*?' is better written without quotes\."),),
+        (logging.INFO,    compile(r"['.*?'] is better written in dot notation\."),),
+        (logging.INFO,    "A dot following a number can be confused with a decimal point.",),
+        (logging.WARNING, "Weird construction. Delete 'new'.",),
+        (logging.WARNING, "Use the object literal notation {}.",),
+        (logging.WARNING, "Use the isNaN function to compare with NaN.",),
+        (logging.WARNING, "Use the array literal notation [].",),
+        (logging.WARNING, compile(r"Use '.*?' to compare with '.*?'\."),),
+        (logging.WARNING, compile(r"Unrecognized tag '<.*?>'\."),),
+        (logging.WARNING, compile(r"Unrecognized attribute '<.*? .*?>'\."),),
+        (logging.WARNING, compile(r"Unreachable '.*?' after '.*?'\."),),
+        (logging.WARNING, "This 'switch' should be an 'if'.",),
+        (logging.WARNING, "'new' should not be used as a statement\.",),
+        (logging.WARNING, compile(r"Label '.*?' on .*? statement\."),),
+        (logging.WARNING, compile(r"Label '.*?' looks like a javascript url\."),),
+        (logging.WARNING, "JavaScript URL.",),
+        (logging.WARNING, "Implied eval is evil. Pass a function instead of a string.",),
+        (logging.WARNING, compile(r"Identifier .*? already declared as .*?\."),),
+        (logging.WARNING, "HTML case error.",),
+        (logging.WARNING, "Expected to see a statement and instead saw a block.",),
+        (logging.WARNING, compile(r"Expected to see a '\(' or '=' or ':' or ',' or '\[' preceding a regular expression literal, and instead saw '.*?'\."),),
+        (logging.WARNING, compile(r"Expected '.*?' to match '.*?' from line .*? and instead saw '.*?'\."),),
+        (logging.WARNING, compile(r"Expected '.*?' to have an indentation at .*? instead at .*?\."),),
+        (logging.WARNING, compile(r"Expected an operator and instead saw '.*?'\."),),
+        (logging.WARNING, "Expected an identifier in an assignment and instead saw a function invocation.   ",),
+        (logging.WARNING, compile(r"Expected an identifier and instead saw '.*?' (a reserved word)\."),),
+        (logging.WARNING, compile(r"Expected an identifier and instead saw '.*?'\."),),
+        (logging.WARNING, "Expected an assignment or function call and instead saw an expression.",),
+        (logging.WARNING, "Expected a 'break' statement before 'default'.",),
+        (logging.WARNING, "Expected a 'break' statement before 'case'.",),
+        (logging.WARNING, compile(r"Expected '.*?' and instead saw '.*?'\."),),
+        (logging.WARNING, "eval is evil.",),
+        (logging.WARNING, "Each value should have its own case label.",),
+        (logging.WARNING, compile(r"Do not use the .*? function as a constructor\."),),
+        (logging.WARNING, "document.write can be a form of eval.",),
+        (logging.WARNING, compile(r"Control character in string: .*?\."),),
+        (logging.WARNING, "All 'debugger' statements should be removed.",),
+        (logging.WARNING, "Adsafe restriction.",),
+        (logging.WARNING, compile(r"Adsafe restricted word '.*?'\."),),
+        (logging.WARNING, "A constructor name should start with an uppercase letter.",),
+        (logging.WARNING, compile(r".*? (.*?% scanned)\."),),
+        (logging.FATAL,   "What the hell is this?",),
+        (logging.ERROR,   compile(r"Variable .*? was used before it was declared\."),),
+        (logging.ERROR,   compile(r"Unmatched '.*?'\."),),
+        (logging.ERROR,   compile(r"Unexpected use of '.*?'\."),),
+        (logging.ERROR,   compile(r"Unexpected space after '.*?'\."),),
+        (logging.ERROR,   "Unexpected early end of program.",),
+        (logging.ERROR,   compile(r"Unexpected characters in '.*?'\."),),
+        (logging.ERROR,   compile(r"Unexpected '.*?'\."),),
+        (logging.ERROR,   compile(r"Undefined .*? '.*?'\."),),
+        (logging.ERROR,   compile(r"Unclosed string\."),),
+        (logging.ERROR,   "Unclosed comment.",),
+        (logging.ERROR,   "Unbegun comment.",),
+        (logging.ERROR,   "The Function constructor is eval.",),
+        (logging.ERROR,   "Nested comment.",),
+        (logging.ERROR,   compile(r"Missing space after '.*?'\."),),
+        (logging.ERROR,   "Missing semicolon.",),
+        (logging.ERROR,   "Missing radix parameter.",),
+        (logging.ERROR,   "Missing quote.",),
+        (logging.ERROR,   "Missing ':' on a case clause.",),
+        (logging.ERROR,   "Missing 'new' prefix when invoking a constructor.",),
+        (logging.ERROR,   "Missing name in function statement.",),
+        (logging.ERROR,   "Missing '()' invoking a constructor.",),
+        (logging.ERROR,   "Missing close quote on script attribute.",),
+        (logging.ERROR,   compile(r"Missing boolean after '.*?'\."),),
+        (logging.ERROR,   compile(r"Missing ':' after '.*?'\."),),
+        (logging.ERROR,   compile(r"Missing '.*?'\."),),
+        (logging.ERROR,   compile(r"Line breaking error '.*?'\."),),
+        (logging.ERROR,   "Function statements are not invocable. Wrap the function expression in parens.   ",),
+        (logging.ERROR,   "Extra comma.",),
+        (logging.ERROR,   compile(r"Bad value '.*?'\."),),
+        (logging.ERROR,   "Bad structure.",),
+        (logging.ERROR,   "Bad regular expression.",),
+        (logging.ERROR,   compile(r"Bad number '.*?'\."),),
+        (logging.ERROR,   compile(r"Bad name '.*?'\."),),
+        (logging.ERROR,   compile(r"Bad jslint option '.*?'\."),),
+        (logging.ERROR,   "Bad invocation.",),
+        (logging.ERROR,   compile(r"Bad extern identifier '.*?'\."),),
+        (logging.ERROR,   "Bad escapement.",),
+        (logging.ERROR,   compile(r".*? .*? declared in a block\."),),
+        (logging.ERROR,   "Bad constructor.",),
+        (logging.ERROR,   "Bad assignment.",),
+        (logging.ERROR,   compile(r"Attribute '.*?' does not belong in '<.*?>'\."),),
+        (logging.ERROR,   "Assignment in control part.",),
+        (logging.ERROR,   compile(r"A '<.*?>' must be within '<.*?>'\."),),
+    )
+
+    @classmethod
+    def get_msg_level(cls, msg, default=logging.ERROR):
+        msg = msg.strip()
+        for level, msg_pat in cls.JSLINT_MSG:
+            if (hasattr(msg_pat, 'match') and msg_pat.match(msg)) or msg == msg_pat:
+                return level
+        else:
+            return default
+
+    def __init__(self, *args, **kwargs):
+        super(JsLintParser, self).__init__(*args, **kwargs)
+        # line, char_idx, msg
+        self._ctx  = None
+
+    def parse_line(self, line):
+        if not line:
+            self._ctx = None
+            return
+        match = self.RE_MSG.match(line)
+        if match:
+            self._ctx = match.groups()
+        elif self._ctx is not None:
+            filepath = self.path
+            lineno = '%s:%s' % (self._ctx[0], self._ctx[1])
+            msg  = self._ctx[2]
+            level = self.get_msg_level(msg)
+            self.writer.log(level, msg, path=filepath, line=lineno)
+            self.set_status(FAILURE)
+        else:
+            self.unparsed.append(line)
+
+
+
+if not call(['which', 'rhino'], stdout=file(os.devnull, 'w')):
+
+    class JsLintChecker(AbstractFilteredFileChecker):
+        """Js Lint checker for each *.js file"""
+
+        id = 'jslint'
+        need_preprocessor = 'build_js'
+        checked_extensions = ('.js', )
+
+        def check_file(self, path):
+            command = ['rhino', JSLINT_PATH, path]
+            return ParsedCommand(self.writer, command, parsercls=JsLintParser,
+                                 path=path).run()
+
+        def version_info(self):
+            super(JsLintChecker, self).version_info()
+            self.record_version_info('jslint', '2010-04-06')
+
+    register('checker', JsLintChecker)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/checkers/apycot/python.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,695 @@
+"""checkers for python source files
+
+:organization: Logilab
+:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: General Public License version 2 - http://www.gnu.org/licenses
+"""
+from __future__ import with_statement
+
+__docformat__ = "restructuredtext en"
+
+import os
+import sys
+import re
+from commands import getoutput
+from os.path import join, exists
+from test import test_support
+from warnings import warn
+from lxml import etree
+
+
+from logilab.common.testlib import find_tests, DEFAULT_PREFIXES
+from logilab.common.modutils import get_module_files
+from logilab.common.decorators import cached
+from logilab.common.compat import any
+from logilab.common.proc import RESOURCE_LIMIT_EXCEPTION
+
+try:
+    import coverage
+    if coverage.__version__ < "3":
+        raise ImportError
+    COVERAGE_CMD = "/usr/bin/python-coverage" # XXX
+except ImportError:
+    coverage = None
+
+from apycotlib import register
+from apycotlib import SUCCESS, FAILURE, PARTIAL, NODATA, ERROR
+from apycotlib import SimpleOutputParser, ParsedCommand
+
+from preprocessors.apycot.distutils import INSTALL_PREFIX, pyversions
+from checkers.apycot import BaseChecker, AbstractFilteredFileChecker
+
+def pyinstall_path(test):
+    path = _pyinstall_path(test)
+    if not exists(path):
+        raise Exception('path %s doesn\'t exist' %  path)
+    return path
+
+@cached
+def _pyinstall_path(test):
+    """return the project's installation path"""
+    config = test.apycot_config()
+    if 'install_path' in config:
+        return config['install_path']
+    modname = config.get('python_modname')
+    if not modname and exists(join(test.tmpdir, test.project_path(), '__pkginfo__.py')):
+        from logilab.devtools.lib.pkginfo import PackageInfo
+        pkginfo = PackageInfo(directory=test.project_path())
+        modname = pkginfo.modname
+        distname = pkginfo.distname or modname
+        package = pkginfo.subpackage_of
+        if modname and package:
+            modname = '%s.%s' % (package, modname)
+        elif distname.startswith('cubicweb-'):
+            cubespdir = join(os.environ['APYCOT_ROOT'], 'local', 'share', 'cubicweb')
+            pypath = cubespdir + os.pathsep + os.environ.get('PYTHONPATH', '')
+            test.update_env(test.tconfig['name'], 'PYTHONPATH', pypath, os.pathsep)
+            return join(cubespdir, 'cubes', modname)
+    if modname:
+        try:
+            path = join(INSTALL_PREFIX[test.project_path()], *modname.split('.'))
+        except KeyError:
+            pass
+        else:
+            cfg = test.apycot_config()
+            if cfg.get('subpath'):
+                path = join(path, cfg['subpath'])
+            return path
+    return test.project_path(subpath=True)
+
+
+class PythonSyntaxChecker(AbstractFilteredFileChecker):
+    """check syntax of python file
+
+       Use Pylint to check a score for python package. The check fails if the score is
+       inferior to a given threshold.
+    """
+    id = 'python_syntax'
+    checked_extensions = ('.py', )
+
+    def check_file(self, filepath):
+        """try to compile the given file to see if it's syntaxicaly correct"""
+        # Try to compile it. If compilation fails, then there's a
+        # SyntaxError
+        try:
+            compile(open(filepath, 'U').read() + '\n', filepath, "exec")
+            return SUCCESS
+        except SyntaxError, error:
+            self.writer.error(error.msg, path=filepath, line=error.lineno)
+            return FAILURE
+
+    def version_info(self):
+        self.record_version_info('python', sys.version)
+
+register('checker', PythonSyntaxChecker)
+
+
+class PyTestParser(SimpleOutputParser):
+    status = NODATA
+    non_zero_status_code = FAILURE
+    # search for following output:
+    #
+    # 'Ran 42 test cases in 0.07s (0.07s CPU), 3 errors, 31 failures, 3 skipped'
+    regex = re.compile(
+        r'Ran (?P<total>[0-9]+) test cases '
+        'in (?P<time>[0-9]+(.[0-9]+)?)s \((?P<cputime>[0-9]+(.[0-9]+)?)s CPU\)'
+        '(, )?'
+        '((?P<errors>[0-9]+) errors)?'
+        '(, )?'
+        '((?P<failures>[0-9]+) failures)?'
+        '(, )?'
+        '((?P<skipped>[0-9]+) skipped)?'
+        )
+
+    total, failures, errors, skipped = 0, 0, 0, 0
+
+    def __init__(self, writer, options=None):
+        super(PyTestParser, self).__init__(writer, options)
+        self.total    = 0
+        self.failures = 0
+        self.skipped  = 0
+        self.errors   = 0
+
+    def _parse(self, stream):
+        self.status = None
+        super(PyTestParser, self)._parse(stream)
+        if self.errors or self.failures:
+            self.set_status(FAILURE)
+        elif self.skipped:
+            self.set_status(PARTIAL)
+        elif not self.total:
+            self.set_status(NODATA)
+        elif self.total >= 0:
+            self.set_status(SUCCESS)
+
+    @property
+    def success(self):
+        return max(0, self.total - sum(( self.failures, self.errors,
+                                         self.skipped,)))
+    def add_junk(self, line):
+        if any(c for c in line if c not in 'EFS. \n\t\r-*'):
+            self.unparsed.append(line)
+
+    def extract_tests_status(self, values):
+        for status in ('failures', 'errors', 'skipped'):
+            try:
+                setattr(self, status,
+                        max(getattr(self, status), int(values[status])))
+            except TypeError:
+                pass
+
+    def parse_line(self, line):
+        match = self.regex.match(line)
+        if match is not None:
+            values = match.groupdict()
+            total = int(values['total'])
+            self.total += total
+            self.extract_tests_status(values)
+        else:
+            self.add_junk(line)
+
+PYVERSIONS_OPTIONS = {
+    'tested_python_versions': {
+        'type': 'csv',
+        'help': ('comma separated list of python version (such as 2.5) that '
+                 'should be considered for testing.'),
+        },
+    'ignored_python_versions': {
+        'type': 'csv',
+        'help': ('comma separated list of python version (such as 2.5) that '
+                 'should be ignored for testing when '
+                 'use_pkginfo_python_versions is set to 1.'),
+        },
+    'use_pkginfo_python_versions': {
+        'type': 'int', 'default': True,
+        'help': ('0/1 flag telling if tested python version should be '
+                 'determinated according to __pkginfo__.pyversion of the '
+                 'tested project. This option is ignored if tested_python_versions '
+                 'is set.'),
+        },
+    'pycoverage': {
+        'type': 'int', 'default': False,
+        'help': ('Tell if test should be run with pycoverage to gather '
+                 'coverage data.'),
+        },
+    }
+
+class PyTestChecker(BaseChecker):
+    """check that unit tests of a python package succeed using the pytest command
+    (from logilab.common)
+    """
+    id = 'pytest'
+    need_preprocessor = 'install'
+    parsercls = PyTestParser
+    parsed_content = 'stdout'
+    options_def = PYVERSIONS_OPTIONS.copy()
+    options_def.update({
+        'pytest_extra_argument': {
+            'type': 'csv',
+            'help': ('extra argument to give to pytest. Add this option multiple '
+                     'times in the correct order to give several arguments.'),
+            },
+        })
+
+    def __init__(self, writer, options=None):
+        BaseChecker.__init__(self, writer, options)
+        self.coverage_data = None
+        self._path = None
+        self.test = None
+
+    def version_info(self):
+        if pyversions(self.test):
+            self.record_version_info('python', ', '.join(pyversions(self.test)))
+
+    def enable_coverage(self):
+        if self.options.get('pycoverage') and coverage:
+            self.coverage_data = join(self.cwd, '.coverage')
+            # XXX we need the environment variable to be considered by
+            # "python-coverage run"
+            os.environ['COVERAGE_FILE'] = self.coverage_data
+            return True
+        return False
+
+    def setup_check(self, test):
+        """run the checker against <path> (usually a directory)"""
+        test_support.verbose = 0
+        self.test = test
+        self.cwd = test.project_path(subpath=True)
+        if not pyversions(self.test):
+            self.writer.error('no required python version available')
+            return ERROR
+        return SUCCESS
+
+    def do_check(self, test):
+        if self.enable_coverage():
+            command = ['-c', 'from logilab.common.pytest import run; import sys; sys.argv=["pytest", "--coverage"]; run()']
+        else:
+            command = ['-c', 'from logilab.common.pytest import run; run()']
+        extraargs = self.options.get("pytest_extra_argument", [])
+        if not isinstance(extraargs, list):
+            command.append(extraargs)
+        else:
+            command += extraargs
+        status = SUCCESS
+        testresults = {'success': 0, 'failures': 0,
+                       'errors': 0, 'skipped': 0}
+        total = 0
+        for python in pyversions(self.test):
+            cmd = self.run_test(command, python)
+            for rtype in testresults:
+                total += getattr(cmd.parser, rtype)
+                testresults[rtype] += getattr(cmd.parser, rtype)
+            status = self.merge_status(status, cmd.status)
+        self.execution_info(total, testresults)
+        return status
+
+    def execution_info(self, total, testresults):
+        self.writer.raw('total_test_cases', total, 'result')
+        self.writer.raw('succeeded_test_cases', testresults['success'], 'result')
+        self.writer.raw('failed_test_cases', testresults['failures'], 'result')
+        self.writer.raw('error_test_cases', testresults['errors'], 'result')
+        self.writer.raw('skipped_test_cases', testresults['skipped'], 'result')
+
+    def get_command(self, command, python):
+        return [python, '-W', 'ignore'] + command
+
+    def run_test(self, command, python='python'):
+        """execute the given test file and parse output to detect failed /
+        succeed test cases
+        """
+        if isinstance(command, basestring):
+            command = [command]
+        command = self.get_command(command, python)
+        cmd = ParsedCommand(self.writer, command,
+                            parsercls=self.parsercls,
+                            parsed_content=self.parsed_content,
+                            path=self._path, cwd=self.cwd)
+        cmd.run()
+        cmd.set_status(cmd.parser.status)
+        return cmd
+
+register('checker', PyTestChecker)
+
+
+class PyUnitTestParser(PyTestParser):
+    result_regex = re.compile(
+        r'(OK|FAILED)'
+        '('
+        ' \('
+        '(failures=(?P<failures>[0-9]+))?'
+        '(, )?'
+        '(errors=(?P<errors>[0-9]+))?'
+        '(, )?'
+        '(skipped=(?P<skipped>[0-9]+))?'
+        '\)'
+        ')?')
+
+    total_regex = re.compile(
+        'Ran (?P<total>[0-9]+) tests?'
+        ' in (?P<time>[0-9]+(.[0-9]+)?s)')
+
+    def parse_line(self, line):
+        match = self.total_regex.match(line)
+        if match is not None:
+            self.total = int(match.groupdict()['total'])
+            return
+        match = self.result_regex.match(line)
+        if match is not None:
+            self.extract_tests_status(match.groupdict())
+            return
+        self.add_junk(line)
+
+
+class PyUnitTestChecker(PyTestChecker):
+    """check that unit tests of a python package succeed
+
+    Execute tests found in the "test" or "tests" directory of the package. The
+    check succeed if no test cases failed. Note each test module is executed by
+    a spawed python interpreter and the output is parsed, so tests should use
+    the default text output of the unittest framework, and avoid messages on
+    stderr.
+
+    spawn unittest and parse output (expect a standard TextTestRunner)
+    """
+    id = 'pyunit'
+    parsed_content = 'stderr'
+    parsercls = PyUnitTestParser
+    options_def = PYVERSIONS_OPTIONS.copy()
+    options_def.update({
+        'test_dirs': {
+            'type': 'csv', 'default': ('test', 'tests'),
+            'help': ('comma separated list of directories where tests could be '
+                     'find. Search in "test" and "tests" by default.'),
+            },
+        'test_prefixes': {
+            'type': 'csv', 'default': DEFAULT_PREFIXES,
+            'help': ('comma separated list of directories where tests could be '
+                     'find. Defaults to %s.' % ', '.join(DEFAULT_PREFIXES)),
+            },
+        })
+
+    def do_check(self, test):
+        status = SUCCESS
+        testdirs = self.options.get("test_dirs")
+        basepath = test.project_path(subpath=True)
+        for testdir in testdirs:
+            testdir = join(basepath, testdir)
+            if exists(testdir):
+                self._path = testdir
+                _status = self.run_tests(testdir)
+                status = self.merge_status(status, _status)
+                break
+        else:
+            self.writer.error('no test directory', path=basepath)
+            status = NODATA
+        return status
+
+    def run_tests(self, testdir):
+        """run a package test suite
+        expect to be in the test directory
+        """
+        tests = find_tests(testdir,
+                           prefixes=self.options.get("test_prefixes"),
+                           remove_suffix=False)
+        if not tests:
+            self.writer.error('no test found', path=self._path)
+            return NODATA
+        status = SUCCESS
+        testresults = {'success': 0, 'failures': 0,
+                       'errors': 0, 'skipped': 0}
+        total = 0
+        for python in pyversions(self.test):
+            for test_file in tests:
+                cmd = self.run_test(join(testdir, test_file), python)
+                total += cmd.parser.total
+                for rtype in testresults:
+                    testresults[rtype] += getattr(cmd.parser, rtype)
+                if cmd.status == NODATA:
+                    self.writer.error('no test found', path=test_file)
+                status = self.merge_status(status, cmd.status)
+        self.execution_info(total, testresults)
+        return status
+
+    def get_command(self, command, python):
+        python = [python, '-W', 'ignore']
+        if self.enable_coverage():
+            python += [COVERAGE_CMD, 'run', '-a', '--branch',
+                       '--source=%s' % pyinstall_path(self.test)]
+        return python + command
+
+register('checker', PyUnitTestChecker)
+
+
+class PyDotTestParser(PyUnitTestParser):
+    line_regex = re.compile(
+            r'(?P<filename>\w+\.py)(\[(?P<ntests>\d+)\] | - )(?P<results>.*)')
+
+    # XXX overwrite property
+    success = 0
+
+    def _parse(self, stream):
+        for _, _, _, results in self.line_regex.findall(stream.read()):
+            if results == "FAILED TO LOAD MODULE":
+                self.errors += 1
+            else:
+                self.success += results.count('.')
+                self.total += results.count('.')
+                self.failures += results.count('F')
+                self.total += results.count('F')
+                self.errors += results.count('E')
+                self.total += results.count('E')
+                self.skipped += results.count('s')
+                self.total += results.count('s')
+        if self.failures or self.errors:
+            self.set_status(FAILURE)
+        elif self.skipped:
+            self.set_status(PARTIAL)
+        elif not self.success:
+            self.set_status(NODATA)
+
+
+class PyDotTestChecker(PyUnitTestChecker):
+    """check that py.test based unit tests of a python package succeed
+
+    spawn py.test and parse output (expect a standard TextTestRunner)
+    """
+    need_preprocessor = 'install'
+    id = 'py.test'
+    parsercls = PyDotTestParser
+    parsed_content = 'stdout'
+    options_def = PYVERSIONS_OPTIONS.copy()
+
+    def get_command(self, command, python):
+        # XXX coverage
+        return ['py.test', '--exec=%s' % python, '--nomagic', '--tb=no'] + command
+
+register('checker', PyDotTestChecker)
+
+
+class PyLintChecker(BaseChecker):
+    """check that the python package as a decent pylint evaluation
+    """
+    need_preprocessor = 'install'
+    id = 'pylint'
+    options_def = {
+        'pylintrc': {
+            'help': ('path to a pylint configuration file.'),
+            },
+        'pylint.threshold': {
+            'type': 'int', 'default': 7,
+            'help': ('integer between 1 and 10 telling expected pylint note to '
+                     'pass this check. Default to 7.'),
+         },
+        'pylint.show_categories': {
+            'type': 'csv', 'default': ['E', 'F'],
+            'help': ('comma separated list of pylint message categories to add to '
+                     'reports. Default to error (E) and failure (F).'),
+         },
+        'pylint.additional_builtins': {
+            'type': 'csv',
+            'help': ('comma separated list of additional builtins to give to '
+                     'pylint.'),
+            },
+        'pylint.disable': {
+            'type': 'csv',
+            'help': ('comma separated list of pylint message id that should be '
+                     'ignored.'),
+            },
+        'pylint.ignore': {
+            'type': 'csv',
+            'help': 'comma separated list of files or directories to ignore',
+            },
+        }
+
+    def version_info(self):
+        self.record_version_info('pylint', pylint_version)
+
+    def do_check(self, test):
+        """run the checker against <path> (usually a directory)"""
+        # XXX should consider python version
+        threshold = self.options.get('pylint.threshold')
+        pylintrc_path = self.options.get('pylintrc')
+        linter = PyLinter(pylintrc=pylintrc_path)
+        # register checkers
+        pycheckers.initialize(linter)
+        # load configuration
+        package_wd_path = test.project_path()
+        if exists(join(package_wd_path, 'pylintrc')):
+            linter.load_file_configuration(join(package_wd_path, 'pylintrc'))
+        else:
+            linter.load_file_configuration()
+        linter.set_option('persistent', False)
+        linter.set_option('reports', 0, action='store')
+        linter.quiet = 1
+        # set file or dir to ignore
+        for option in ('ignore', 'additional_builtins', 'disable'):
+            value = self.options.get('pylint.' + option)
+            if value is not None:
+                linter.global_set_option(option.replace('_', '-'), ','.join(value))
+        # message categories to record
+        categories = self.options.get('pylint.show_categories')
+        linter.set_reporter(MyLintReporter(self.writer, test.tmpdir, categories))
+        # run pylint
+        linter.check(pyinstall_path(test))
+        try:
+            note = eval(linter.config.evaluation, {}, linter.stats)
+            self.writer.raw('pylint.evaluation', '%.2f' % note, 'result')
+        except ZeroDivisionError:
+            self.writer.raw('pylint.evaluation', '0', 'result')
+            note = 0
+        except RESOURCE_LIMIT_EXCEPTION:
+            raise
+        except Exception:
+            self.writer.error('Error while processing pylint evaluation',
+                              path=test.project_path(subpath=True), tb=True)
+            note = 0
+        self.writer.raw('statements', '%i' % linter.stats['statement'], 'result')
+        if note < threshold:
+            return FAILURE
+        return SUCCESS
+
+try:
+    from pylint import checkers as pycheckers
+    from pylint.lint import PyLinter
+    from pylint.__pkginfo__ import version as pylint_version
+    from pylint.interfaces import IReporter
+    from pylint.reporters import BaseReporter
+    register('checker', PyLintChecker)
+
+    class MyLintReporter(BaseReporter):
+        """a partial pylint writer (implements only the message method, not
+        methods necessary to display layouts
+        """
+        __implements__ = IReporter
+
+        def __init__(self, writer, basepath, categories):
+            self.writer = writer
+            self.categories = set(categories)
+            self._to_remove = len(basepath) + 1 # +1 for the leading "/"
+
+        def add_message(self, msg_id, location, msg):
+            """ manage message of different type and in the context of path """
+            if not msg_id[0] in self.categories:
+                return
+            path, line = location[0], location[-1]
+            path = path[self._to_remove:]
+            if msg_id[0] == 'I':
+                self.writer.info(msg, path=path, line=line)
+            elif msg_id[0]  == 'E':
+                self.writer.error(msg, path=path, line=line)
+            elif msg_id[0] == 'F':
+                self.writer.fatal(msg, path=path, line=line)
+            else: # msg_id[0] in ('R', 'C', 'W')
+                self.writer.warning(msg, path=path, line=line)
+
+        def display_results(self, layout):
+            pass
+except ImportError, e:
+    warn("unable to import pylint. Pylint checker disabled : %s" % e)
+
+
+class PyCoverageChecker(BaseChecker):
+    """retrieve the tests coverage data
+
+    The coverage data are coming from the pyunit checker with the "pycoverage"
+    configuration variable
+
+    When devtools is available, test will be launched in a coverage mode. This
+    test will gather coverage information, and will succeed if the test coverage
+    is superior to a given threshold. *This checker must be executed after the
+    python_unittest checker.
+    """
+    id = 'pycoverage'
+    options_def = {
+        'coverage_threshold': {
+            'type': 'int', 'default': 80,
+            'help': ('integer between 1 and 100 telling expected percent coverage '
+                     'to pass this check. Default to 80.\n'
+                     'PARTIAL returned when cover rate between threshold and threshold / 2.\n'
+                     'ERROR returned when cover rate under threshold / 2'),
+        },
+        'coverage_data': {
+            'required': True,
+            'help': 'collect coverage data file',
+        },
+    }
+
+    def version_info(self):
+        if coverage:
+            version = getoutput('%s --version' % COVERAGE_CMD).strip()
+            self.record_version_info('python-coverage', version)
+
+    def do_check(self, test):
+        """run the checker against <path> (usually a directory)"""
+        self.threshold = float(self.options.get('coverage_threshold')) / 100
+        coverage_data = self.options.get('coverage_data')
+        if coverage_data == None or not exists(coverage_data):
+            self.writer.fatal('no coverage information', path=coverage_data)
+            return NODATA
+        line_rate, branch_rate = self._get_cover_info(test)
+        # in case of error during coverage reporting
+        if line_rate is None:
+            return ERROR
+        # global summary
+        self.writer.raw('cover-line-rate', '%.3f' % line_rate, 'result')
+        self.writer.raw('cover-branch-rate', '%.3f' % branch_rate, 'result')
+        if line_rate < self.threshold:
+            return FAILURE
+        return SUCCESS
+
+    def _get_log_method(self, pc_cover):
+        if pc_cover < (self.threshold / 2):
+            writer = self.writer.error
+        elif pc_cover < self.threshold:
+            writer = self.writer.warning
+        else:
+            writer = self.writer.info
+        return writer
+
+    def _get_cover_info(self, test):
+        if coverage is None:
+            raise Exception('install python-coverage')
+        covertool = coverage.coverage()
+        covertool.use_cache(self.options.get('coverage_data'))
+        covertool.load()
+        try:
+            report_file = join(test.project_path(), "coverage.xml")
+            covertool.xml_report(outfile=report_file, ignore_errors=True)
+            report = etree.parse(report_file).getroot()
+            pc_cover = float(report.attrib.get('line-rate'))
+            br_rate  = float(report.attrib.get('branch-rate'))
+            # format of the xml_report file is compatible with Cobertura
+            # <http://cobertura.sourceforge.net/>
+            #
+            # FIXME missing stats: <stat> / <miss>
+            # FIXME p_name construction (arbitrary slice)
+            for package in report.iter("package"):
+                p_name = package.attrib.get('name')
+                p_name = ".".join(p_name.split(".")[6:])
+                # class *are* files in Coberture (from java world)
+                for cls in package.iter("class"):
+                    c_name = ".".join([p_name, cls.attrib.get('name')])
+                    c_pc_cover = float(cls.attrib.get('line-rate'))
+                    c_br_rate  = float(cls.attrib.get('branch-rate'))
+                    logger = self._get_log_method(c_pc_cover)
+                    logger("line rate: %3.0f %% / branch rate: %3.0f %%"
+                           % (c_pc_cover*100, c_br_rate*100), path=c_name)
+        except Exception, err:
+            pc_cover = br_rate = None
+            self.writer.fatal(err, tb=True)
+        finally:
+            return (pc_cover, br_rate)
+
+if coverage is not None:
+    register('checker', PyCoverageChecker)
+
+
+class PyCheckerOutputParser(SimpleOutputParser):
+    non_zero_status_code = FAILURE
+    def parse_line(self, line):
+        try:
+            path, line, msg = line.split(':')
+            self.writer.error(msg, path=path, line=line)
+            self.status = FAILURE
+        except ValueError:
+            self.unparsed.append(line)
+
+class PyCheckerChecker(BaseChecker):
+    """check that unit tests of a python package succeed
+
+    spawn unittest and parse output (expect a standard TextTestRunner)
+    """
+    id = 'pychecker'
+    need_preprocessor = 'install'
+
+    def do_check(self, test):
+        """run the checker against <path> (usually a directory)"""
+        command = ['pychecker', '-Qqe', 'Style']
+        command += get_module_files(pyinstall_path(test))
+        return ParsedCommand(self.writer, command, parsercls=PyCheckerOutputParser).run()
+
+    def version_info(self):
+        self.record_version_info('pychecker', getoutput("pychecker --version").strip())
+
+register('checker', PyCheckerChecker)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/checkers/apycot/scenarios.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,33 @@
+import os
+from commands import getstatusoutput
+from apycotlib import SUCCESS, FAILURE, ERROR
+from apycotlib import register
+
+from checkers.apycot import AbstractFilteredFileChecker
+
+class ScriptRunner(AbstractFilteredFileChecker):
+    """
+    run files accepted by the filter
+    """
+    id = 'script_runner'
+    def do_check(self, test):
+        if self.options.get('filename_filter') is not None:
+            self.filename_filter = self.options.get('filename_filter')
+        super(ScriptRunner, self).do_check(test)
+
+    def check_file(self, filepath):
+        try:
+            self.writer.debug("running : " + filepath, path=filepath)
+            status, out = getstatusoutput(filepath)
+            if status != 0:
+                self.writer.error(out, path=filepath)
+                return FAILURE
+            self.writer.info(out, path=filepath)
+            return SUCCESS
+        except Exception, error:
+            self.writer.error(error.msg, path=filepath)
+            return ERROR
+
+register('checker', ScriptRunner)
+
+
--- a/_apycotlib/checkers/debian.py	Fri Jan 17 18:32:07 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-import apycotlib
-import logging
-import os
-import subprocess
-from checkers.apycot import BaseChecker
-
-def find_with_extension(path, extension):
-    path = os.path.expanduser(path)
-    for folder, subfolders, files in os.walk(path):
-        for filename in files:
-            if filename.endswith(extension):
-                yield os.path.join(folder, filename)
-
-class DebianLintianChecker(BaseChecker):
-    """Runs the lintian program after building a debian package. Lintian
-    checks for bugs and debian policy violations."""
-
-    id = 'lintian'
-
-    checked_extensions = ('.changes',)
-    options_def = {
-        'changes-files': {
-            'type': 'csv',
-            'required': False,
-            'help': 'changes files to check',
-        },
-    }
-
-    def get_output(self, path):
-        cmd = subprocess.Popen(['lintian', '-I', '--suppress-tags',
-                                'bad-distribution-in-changes-file', path],
-                               stdout=subprocess.PIPE, stdin=open('/dev/null'),
-                               stderr=subprocess.STDOUT)
-        for line in cmd.stdout:
-            yield line
-        cmd.wait()
-
-    def do_check(self, test):
-        status = apycotlib.SUCCESS
-        build_folder = os.path.join(test.project_path(), os.pardir)
-        change_files = find_with_extension(build_folder, '.changes')
-        if not change_files:
-            status = apycotlib.NODATA
-        for f in change_files:
-            iter_line = self.get_output(f)
-            for line in iter_line:
-                line_parts = line.split(':', 1)
-                try:
-                    mtype, msg = line_parts
-                except ValueError:
-                    self.writer.fatal('unexpected line %r' % line, path=f)
-                    for line in iter_line:
-                        self.writer.info('followed by: %r' % line, path=f)
-                    return apycotlib.ERROR
-                else:
-                    if mtype == 'I':
-                        self.writer.info(msg, path=f)
-                    elif mtype == 'W':
-                        self.writer.warning(msg, path=f)
-                    elif mtype == 'E':
-                        self.writer.error(msg, path=f)
-                        status = apycotlib.FAILURE
-                    else:
-                        self.writer.info(msg, path=f)
-        return status
-
-apycotlib.register('checker', DebianLintianChecker)
-
--- a/_apycotlib/checkers/jslint.py	Fri Jan 17 18:32:07 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,178 +0,0 @@
-import re
-import os
-import logging
-from subprocess import call
-from os.path import exists, dirname, join, abspath
-from re import compile
-
-from apycotlib import register, OutputParser, ParsedCommand, FAILURE
-from checkers.apycot import AbstractFilteredFileChecker
-
-# in source installation, jslint.js is in apycot/ext, we're currently in
-# apycot/_apycotlib/checkers
-JSLINT_PATH = join(dirname(dirname(dirname(abspath(__file__)))), 'ext', 'jslint.js')
-if not exists(JSLINT_PATH):
-    JSLINT_PATH = '/usr/share/narval/data/apycot/jslint.js'
-
-class JsLintParser(OutputParser):
-    """Simple Parser class interpretting
-
-    Lint at line 8 character 1: 'CubicWeb' is not defined.
-    CubicWeb.require('htmlhelpers.js');
-    """
-    non_zero_status_code = FAILURE
-
-    RE_MSG = re.compile(r'^Lint at line (\d+) character (\d+):\s*(.*)$')
-    #RE_NO_ISSUE = re.compile(r'^jslint: No problems found in ')
-
-    JSLINT_MSG = (
-        (logging.INFO,    "Unnecessary semicolon.",),
-        (logging.INFO,    "Unnecessary escapement.",),
-        (logging.INFO,    "The 'language' attribute is deprecated.",),
-        (logging.INFO,    "Inner functions should be listed at the top of the outer function.",),
-        (logging.INFO,    compile(r"Don't use extra leading zeros '.*?'\."),),
-        (logging.INFO,    "Confusing plusses.",),
-        (logging.INFO,    "Confusing minusses.",),
-        (logging.INFO,    compile(r"A trailing decimal point can be confused with a dot '.*?'i\."),),
-        (logging.INFO,    compile(r"A leading decimal point can be confused with a dot '\..*?'\."),),
-        (logging.INFO,    compile(r"'.*?' is better written without quotes\."),),
-        (logging.INFO,    compile(r"['.*?'] is better written in dot notation\."),),
-        (logging.INFO,    "A dot following a number can be confused with a decimal point.",),
-        (logging.WARNING, "Weird construction. Delete 'new'.",),
-        (logging.WARNING, "Use the object literal notation {}.",),
-        (logging.WARNING, "Use the isNaN function to compare with NaN.",),
-        (logging.WARNING, "Use the array literal notation [].",),
-        (logging.WARNING, compile(r"Use '.*?' to compare with '.*?'\."),),
-        (logging.WARNING, compile(r"Unrecognized tag '<.*?>'\."),),
-        (logging.WARNING, compile(r"Unrecognized attribute '<.*? .*?>'\."),),
-        (logging.WARNING, compile(r"Unreachable '.*?' after '.*?'\."),),
-        (logging.WARNING, "This 'switch' should be an 'if'.",),
-        (logging.WARNING, "'new' should not be used as a statement\.",),
-        (logging.WARNING, compile(r"Label '.*?' on .*? statement\."),),
-        (logging.WARNING, compile(r"Label '.*?' looks like a javascript url\."),),
-        (logging.WARNING, "JavaScript URL.",),
-        (logging.WARNING, "Implied eval is evil. Pass a function instead of a string.",),
-        (logging.WARNING, compile(r"Identifier .*? already declared as .*?\."),),
-        (logging.WARNING, "HTML case error.",),
-        (logging.WARNING, "Expected to see a statement and instead saw a block.",),
-        (logging.WARNING, compile(r"Expected to see a '\(' or '=' or ':' or ',' or '\[' preceding a regular expression literal, and instead saw '.*?'\."),),
-        (logging.WARNING, compile(r"Expected '.*?' to match '.*?' from line .*? and instead saw '.*?'\."),),
-        (logging.WARNING, compile(r"Expected '.*?' to have an indentation at .*? instead at .*?\."),),
-        (logging.WARNING, compile(r"Expected an operator and instead saw '.*?'\."),),
-        (logging.WARNING, "Expected an identifier in an assignment and instead saw a function invocation.   ",),
-        (logging.WARNING, compile(r"Expected an identifier and instead saw '.*?' (a reserved word)\."),),
-        (logging.WARNING, compile(r"Expected an identifier and instead saw '.*?'\."),),
-        (logging.WARNING, "Expected an assignment or function call and instead saw an expression.",),
-        (logging.WARNING, "Expected a 'break' statement before 'default'.",),
-        (logging.WARNING, "Expected a 'break' statement before 'case'.",),
-        (logging.WARNING, compile(r"Expected '.*?' and instead saw '.*?'\."),),
-        (logging.WARNING, "eval is evil.",),
-        (logging.WARNING, "Each value should have its own case label.",),
-        (logging.WARNING, compile(r"Do not use the .*? function as a constructor\."),),
-        (logging.WARNING, "document.write can be a form of eval.",),
-        (logging.WARNING, compile(r"Control character in string: .*?\."),),
-        (logging.WARNING, "All 'debugger' statements should be removed.",),
-        (logging.WARNING, "Adsafe restriction.",),
-        (logging.WARNING, compile(r"Adsafe restricted word '.*?'\."),),
-        (logging.WARNING, "A constructor name should start with an uppercase letter.",),
-        (logging.WARNING, compile(r".*? (.*?% scanned)\."),),
-        (logging.FATAL,   "What the hell is this?",),
-        (logging.ERROR,   compile(r"Variable .*? was used before it was declared\."),),
-        (logging.ERROR,   compile(r"Unmatched '.*?'\."),),
-        (logging.ERROR,   compile(r"Unexpected use of '.*?'\."),),
-        (logging.ERROR,   compile(r"Unexpected space after '.*?'\."),),
-        (logging.ERROR,   "Unexpected early end of program.",),
-        (logging.ERROR,   compile(r"Unexpected characters in '.*?'\."),),
-        (logging.ERROR,   compile(r"Unexpected '.*?'\."),),
-        (logging.ERROR,   compile(r"Undefined .*? '.*?'\."),),
-        (logging.ERROR,   compile(r"Unclosed string\."),),
-        (logging.ERROR,   "Unclosed comment.",),
-        (logging.ERROR,   "Unbegun comment.",),
-        (logging.ERROR,   "The Function constructor is eval.",),
-        (logging.ERROR,   "Nested comment.",),
-        (logging.ERROR,   compile(r"Missing space after '.*?'\."),),
-        (logging.ERROR,   "Missing semicolon.",),
-        (logging.ERROR,   "Missing radix parameter.",),
-        (logging.ERROR,   "Missing quote.",),
-        (logging.ERROR,   "Missing ':' on a case clause.",),
-        (logging.ERROR,   "Missing 'new' prefix when invoking a constructor.",),
-        (logging.ERROR,   "Missing name in function statement.",),
-        (logging.ERROR,   "Missing '()' invoking a constructor.",),
-        (logging.ERROR,   "Missing close quote on script attribute.",),
-        (logging.ERROR,   compile(r"Missing boolean after '.*?'\."),),
-        (logging.ERROR,   compile(r"Missing ':' after '.*?'\."),),
-        (logging.ERROR,   compile(r"Missing '.*?'\."),),
-        (logging.ERROR,   compile(r"Line breaking error '.*?'\."),),
-        (logging.ERROR,   "Function statements are not invocable. Wrap the function expression in parens.   ",),
-        (logging.ERROR,   "Extra comma.",),
-        (logging.ERROR,   compile(r"Bad value '.*?'\."),),
-        (logging.ERROR,   "Bad structure.",),
-        (logging.ERROR,   "Bad regular expression.",),
-        (logging.ERROR,   compile(r"Bad number '.*?'\."),),
-        (logging.ERROR,   compile(r"Bad name '.*?'\."),),
-        (logging.ERROR,   compile(r"Bad jslint option '.*?'\."),),
-        (logging.ERROR,   "Bad invocation.",),
-        (logging.ERROR,   compile(r"Bad extern identifier '.*?'\."),),
-        (logging.ERROR,   "Bad escapement.",),
-        (logging.ERROR,   compile(r".*? .*? declared in a block\."),),
-        (logging.ERROR,   "Bad constructor.",),
-        (logging.ERROR,   "Bad assignment.",),
-        (logging.ERROR,   compile(r"Attribute '.*?' does not belong in '<.*?>'\."),),
-        (logging.ERROR,   "Assignment in control part.",),
-        (logging.ERROR,   compile(r"A '<.*?>' must be within '<.*?>'\."),),
-    )
-
-    @classmethod
-    def get_msg_level(cls, msg, default=logging.ERROR):
-        msg = msg.strip()
-        for level, msg_pat in cls.JSLINT_MSG:
-            if (hasattr(msg_pat, 'match') and msg_pat.match(msg)) or msg == msg_pat:
-                return level
-        else:
-            return default
-
-    def __init__(self, *args, **kwargs):
-        super(JsLintParser, self).__init__(*args, **kwargs)
-        # line, char_idx, msg
-        self._ctx  = None
-
-    def parse_line(self, line):
-        if not line:
-            self._ctx = None
-            return
-        match = self.RE_MSG.match(line)
-        if match:
-            self._ctx = match.groups()
-        elif self._ctx is not None:
-            filepath = self.path
-            lineno = '%s:%s' % (self._ctx[0], self._ctx[1])
-            msg  = self._ctx[2]
-            level = self.get_msg_level(msg)
-            self.writer.log(level, msg, path=filepath, line=lineno)
-            self.set_status(FAILURE)
-        else:
-            self.unparsed.append(line)
-
-
-
-if not call(['which', 'rhino'], stdout=file(os.devnull, 'w')):
-
-    class JsLintChecker(AbstractFilteredFileChecker):
-        """Js Lint checker for each *.js file"""
-
-        id = 'jslint'
-        need_preprocessor = 'build_js'
-        checked_extensions = ('.js', )
-
-        def check_file(self, path):
-            command = ['rhino', JSLINT_PATH, path]
-            return ParsedCommand(self.writer, command, parsercls=JsLintParser,
-                                 path=path).run()
-
-        def version_info(self):
-            super(JsLintChecker, self).version_info()
-            self.record_version_info('jslint', '2010-04-06')
-
-    register('checker', JsLintChecker)
-
-
--- a/_apycotlib/checkers/python.py	Fri Jan 17 18:32:07 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,695 +0,0 @@
-"""checkers for python source files
-
-:organization: Logilab
-:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: General Public License version 2 - http://www.gnu.org/licenses
-"""
-from __future__ import with_statement
-
-__docformat__ = "restructuredtext en"
-
-import os
-import sys
-import re
-from commands import getoutput
-from os.path import join, exists
-from test import test_support
-from warnings import warn
-from lxml import etree
-
-
-from logilab.common.testlib import find_tests, DEFAULT_PREFIXES
-from logilab.common.modutils import get_module_files
-from logilab.common.decorators import cached
-from logilab.common.compat import any
-from logilab.common.proc import RESOURCE_LIMIT_EXCEPTION
-
-try:
-    import coverage
-    if coverage.__version__ < "3":
-        raise ImportError
-    COVERAGE_CMD = "/usr/bin/python-coverage" # XXX
-except ImportError:
-    coverage = None
-
-from apycotlib import register
-from apycotlib import SUCCESS, FAILURE, PARTIAL, NODATA, ERROR
-from apycotlib import SimpleOutputParser, ParsedCommand
-
-from preprocessors.apycot.distutils import INSTALL_PREFIX, pyversions
-from checkers.apycot import BaseChecker, AbstractFilteredFileChecker
-
-def pyinstall_path(test):
-    path = _pyinstall_path(test)
-    if not exists(path):
-        raise Exception('path %s doesn\'t exist' %  path)
-    return path
-
-@cached
-def _pyinstall_path(test):
-    """return the project's installation path"""
-    config = test.apycot_config()
-    if 'install_path' in config:
-        return config['install_path']
-    modname = config.get('python_modname')
-    if not modname and exists(join(test.tmpdir, test.project_path(), '__pkginfo__.py')):
-        from logilab.devtools.lib.pkginfo import PackageInfo
-        pkginfo = PackageInfo(directory=test.project_path())
-        modname = pkginfo.modname
-        distname = pkginfo.distname or modname
-        package = pkginfo.subpackage_of
-        if modname and package:
-            modname = '%s.%s' % (package, modname)
-        elif distname.startswith('cubicweb-'):
-            cubespdir = join(os.environ['APYCOT_ROOT'], 'local', 'share', 'cubicweb')
-            pypath = cubespdir + os.pathsep + os.environ.get('PYTHONPATH', '')
-            test.update_env(test.tconfig['name'], 'PYTHONPATH', pypath, os.pathsep)
-            return join(cubespdir, 'cubes', modname)
-    if modname:
-        try:
-            path = join(INSTALL_PREFIX[test.project_path()], *modname.split('.'))
-        except KeyError:
-            pass
-        else:
-            cfg = test.apycot_config()
-            if cfg.get('subpath'):
-                path = join(path, cfg['subpath'])
-            return path
-    return test.project_path(subpath=True)
-
-
-class PythonSyntaxChecker(AbstractFilteredFileChecker):
-    """check syntax of python file
-
-       Use Pylint to check a score for python package. The check fails if the score is
-       inferior to a given threshold.
-    """
-    id = 'python_syntax'
-    checked_extensions = ('.py', )
-
-    def check_file(self, filepath):
-        """try to compile the given file to see if it's syntaxicaly correct"""
-        # Try to compile it. If compilation fails, then there's a
-        # SyntaxError
-        try:
-            compile(open(filepath, 'U').read() + '\n', filepath, "exec")
-            return SUCCESS
-        except SyntaxError, error:
-            self.writer.error(error.msg, path=filepath, line=error.lineno)
-            return FAILURE
-
-    def version_info(self):
-        self.record_version_info('python', sys.version)
-
-register('checker', PythonSyntaxChecker)
-
-
-class PyTestParser(SimpleOutputParser):
-    status = NODATA
-    non_zero_status_code = FAILURE
-    # search for following output:
-    #
-    # 'Ran 42 test cases in 0.07s (0.07s CPU), 3 errors, 31 failures, 3 skipped'
-    regex = re.compile(
-        r'Ran (?P<total>[0-9]+) test cases '
-        'in (?P<time>[0-9]+(.[0-9]+)?)s \((?P<cputime>[0-9]+(.[0-9]+)?)s CPU\)'
-        '(, )?'
-        '((?P<errors>[0-9]+) errors)?'
-        '(, )?'
-        '((?P<failures>[0-9]+) failures)?'
-        '(, )?'
-        '((?P<skipped>[0-9]+) skipped)?'
-        )
-
-    total, failures, errors, skipped = 0, 0, 0, 0
-
-    def __init__(self, writer, options=None):
-        super(PyTestParser, self).__init__(writer, options)
-        self.total    = 0
-        self.failures = 0
-        self.skipped  = 0
-        self.errors   = 0
-
-    def _parse(self, stream):
-        self.status = None
-        super(PyTestParser, self)._parse(stream)
-        if self.errors or self.failures:
-            self.set_status(FAILURE)
-        elif self.skipped:
-            self.set_status(PARTIAL)
-        elif not self.total:
-            self.set_status(NODATA)
-        elif self.total >= 0:
-            self.set_status(SUCCESS)
-
-    @property
-    def success(self):
-        return max(0, self.total - sum(( self.failures, self.errors,
-                                         self.skipped,)))
-    def add_junk(self, line):
-        if any(c for c in line if c not in 'EFS. \n\t\r-*'):
-            self.unparsed.append(line)
-
-    def extract_tests_status(self, values):
-        for status in ('failures', 'errors', 'skipped'):
-            try:
-                setattr(self, status,
-                        max(getattr(self, status), int(values[status])))
-            except TypeError:
-                pass
-
-    def parse_line(self, line):
-        match = self.regex.match(line)
-        if match is not None:
-            values = match.groupdict()
-            total = int(values['total'])
-            self.total += total
-            self.extract_tests_status(values)
-        else:
-            self.add_junk(line)
-
-PYVERSIONS_OPTIONS = {
-    'tested_python_versions': {
-        'type': 'csv',
-        'help': ('comma separated list of python version (such as 2.5) that '
-                 'should be considered for testing.'),
-        },
-    'ignored_python_versions': {
-        'type': 'csv',
-        'help': ('comma separated list of python version (such as 2.5) that '
-                 'should be ignored for testing when '
-                 'use_pkginfo_python_versions is set to 1.'),
-        },
-    'use_pkginfo_python_versions': {
-        'type': 'int', 'default': True,
-        'help': ('0/1 flag telling if tested python version should be '
-                 'determinated according to __pkginfo__.pyversion of the '
-                 'tested project. This option is ignored if tested_python_versions '
-                 'is set.'),
-        },
-    'pycoverage': {
-        'type': 'int', 'default': False,
-        'help': ('Tell if test should be run with pycoverage to gather '
-                 'coverage data.'),
-        },
-    }
-
-class PyTestChecker(BaseChecker):
-    """check that unit tests of a python package succeed using the pytest command
-    (from logilab.common)
-    """
-    id = 'pytest'
-    need_preprocessor = 'install'
-    parsercls = PyTestParser
-    parsed_content = 'stdout'
-    options_def = PYVERSIONS_OPTIONS.copy()
-    options_def.update({
-        'pytest_extra_argument': {
-            'type': 'csv',
-            'help': ('extra argument to give to pytest. Add this option multiple '
-                     'times in the correct order to give several arguments.'),
-            },
-        })
-
-    def __init__(self, writer, options=None):
-        BaseChecker.__init__(self, writer, options)
-        self.coverage_data = None
-        self._path = None
-        self.test = None
-
-    def version_info(self):
-        if pyversions(self.test):
-            self.record_version_info('python', ', '.join(pyversions(self.test)))
-
-    def enable_coverage(self):
-        if self.options.get('pycoverage') and coverage:
-            self.coverage_data = join(self.cwd, '.coverage')
-            # XXX we need the environment variable to be considered by
-            # "python-coverage run"
-            os.environ['COVERAGE_FILE'] = self.coverage_data
-            return True
-        return False
-
-    def setup_check(self, test):
-        """run the checker against <path> (usually a directory)"""
-        test_support.verbose = 0
-        self.test = test
-        self.cwd = test.project_path(subpath=True)
-        if not pyversions(self.test):
-            self.writer.error('no required python version available')
-            return ERROR
-        return SUCCESS
-
-    def do_check(self, test):
-        if self.enable_coverage():
-            command = ['-c', 'from logilab.common.pytest import run; import sys; sys.argv=["pytest", "--coverage"]; run()']
-        else:
-            command = ['-c', 'from logilab.common.pytest import run; run()']
-        extraargs = self.options.get("pytest_extra_argument", [])
-        if not isinstance(extraargs, list):
-            command.append(extraargs)
-        else:
-            command += extraargs
-        status = SUCCESS
-        testresults = {'success': 0, 'failures': 0,
-                       'errors': 0, 'skipped': 0}
-        total = 0
-        for python in pyversions(self.test):
-            cmd = self.run_test(command, python)
-            for rtype in testresults:
-                total += getattr(cmd.parser, rtype)
-                testresults[rtype] += getattr(cmd.parser, rtype)
-            status = self.merge_status(status, cmd.status)
-        self.execution_info(total, testresults)
-        return status
-
-    def execution_info(self, total, testresults):
-        self.writer.raw('total_test_cases', total, 'result')
-        self.writer.raw('succeeded_test_cases', testresults['success'], 'result')
-        self.writer.raw('failed_test_cases', testresults['failures'], 'result')
-        self.writer.raw('error_test_cases', testresults['errors'], 'result')
-        self.writer.raw('skipped_test_cases', testresults['skipped'], 'result')
-
-    def get_command(self, command, python):
-        return [python, '-W', 'ignore'] + command
-
-    def run_test(self, command, python='python'):
-        """execute the given test file and parse output to detect failed /
-        succeed test cases
-        """
-        if isinstance(command, basestring):
-            command = [command]
-        command = self.get_command(command, python)
-        cmd = ParsedCommand(self.writer, command,
-                            parsercls=self.parsercls,
-                            parsed_content=self.parsed_content,
-                            path=self._path, cwd=self.cwd)
-        cmd.run()
-        cmd.set_status(cmd.parser.status)
-        return cmd
-
-register('checker', PyTestChecker)
-
-
-class PyUnitTestParser(PyTestParser):
-    result_regex = re.compile(
-        r'(OK|FAILED)'
-        '('
-        ' \('
-        '(failures=(?P<failures>[0-9]+))?'
-        '(, )?'
-        '(errors=(?P<errors>[0-9]+))?'
-        '(, )?'
-        '(skipped=(?P<skipped>[0-9]+))?'
-        '\)'
-        ')?')
-
-    total_regex = re.compile(
-        'Ran (?P<total>[0-9]+) tests?'
-        ' in (?P<time>[0-9]+(.[0-9]+)?s)')
-
-    def parse_line(self, line):
-        match = self.total_regex.match(line)
-        if match is not None:
-            self.total = int(match.groupdict()['total'])
-            return
-        match = self.result_regex.match(line)
-        if match is not None:
-            self.extract_tests_status(match.groupdict())
-            return
-        self.add_junk(line)
-
-
-class PyUnitTestChecker(PyTestChecker):
-    """check that unit tests of a python package succeed
-
-    Execute tests found in the "test" or "tests" directory of the package. The
-    check succeed if no test cases failed. Note each test module is executed by
-    a spawed python interpreter and the output is parsed, so tests should use
-    the default text output of the unittest framework, and avoid messages on
-    stderr.
-
-    spawn unittest and parse output (expect a standard TextTestRunner)
-    """
-    id = 'pyunit'
-    parsed_content = 'stderr'
-    parsercls = PyUnitTestParser
-    options_def = PYVERSIONS_OPTIONS.copy()
-    options_def.update({
-        'test_dirs': {
-            'type': 'csv', 'default': ('test', 'tests'),
-            'help': ('comma separated list of directories where tests could be '
-                     'find. Search in "test" and "tests" by default.'),
-            },
-        'test_prefixes': {
-            'type': 'csv', 'default': DEFAULT_PREFIXES,
-            'help': ('comma separated list of directories where tests could be '
-                     'find. Defaults to %s.' % ', '.join(DEFAULT_PREFIXES)),
-            },
-        })
-
-    def do_check(self, test):
-        status = SUCCESS
-        testdirs = self.options.get("test_dirs")
-        basepath = test.project_path(subpath=True)
-        for testdir in testdirs:
-            testdir = join(basepath, testdir)
-            if exists(testdir):
-                self._path = testdir
-                _status = self.run_tests(testdir)
-                status = self.merge_status(status, _status)
-                break
-        else:
-            self.writer.error('no test directory', path=basepath)
-            status = NODATA
-        return status
-
-    def run_tests(self, testdir):
-        """run a package test suite
-        expect to be in the test directory
-        """
-        tests = find_tests(testdir,
-                           prefixes=self.options.get("test_prefixes"),
-                           remove_suffix=False)
-        if not tests:
-            self.writer.error('no test found', path=self._path)
-            return NODATA
-        status = SUCCESS
-        testresults = {'success': 0, 'failures': 0,
-                       'errors': 0, 'skipped': 0}
-        total = 0
-        for python in pyversions(self.test):
-            for test_file in tests:
-                cmd = self.run_test(join(testdir, test_file), python)
-                total += cmd.parser.total
-                for rtype in testresults:
-                    testresults[rtype] += getattr(cmd.parser, rtype)
-                if cmd.status == NODATA:
-                    self.writer.error('no test found', path=test_file)
-                status = self.merge_status(status, cmd.status)
-        self.execution_info(total, testresults)
-        return status
-
-    def get_command(self, command, python):
-        python = [python, '-W', 'ignore']
-        if self.enable_coverage():
-            python += [COVERAGE_CMD, 'run', '-a', '--branch',
-                       '--source=%s' % pyinstall_path(self.test)]
-        return python + command
-
-register('checker', PyUnitTestChecker)
-
-
-class PyDotTestParser(PyUnitTestParser):
-    line_regex = re.compile(
-            r'(?P<filename>\w+\.py)(\[(?P<ntests>\d+)\] | - )(?P<results>.*)')
-
-    # XXX overwrite property
-    success = 0
-
-    def _parse(self, stream):
-        for _, _, _, results in self.line_regex.findall(stream.read()):
-            if results == "FAILED TO LOAD MODULE":
-                self.errors += 1
-            else:
-                self.success += results.count('.')
-                self.total += results.count('.')
-                self.failures += results.count('F')
-                self.total += results.count('F')
-                self.errors += results.count('E')
-                self.total += results.count('E')
-                self.skipped += results.count('s')
-                self.total += results.count('s')
-        if self.failures or self.errors:
-            self.set_status(FAILURE)
-        elif self.skipped:
-            self.set_status(PARTIAL)
-        elif not self.success:
-            self.set_status(NODATA)
-
-
-class PyDotTestChecker(PyUnitTestChecker):
-    """check that py.test based unit tests of a python package succeed
-
-    spawn py.test and parse output (expect a standard TextTestRunner)
-    """
-    need_preprocessor = 'install'
-    id = 'py.test'
-    parsercls = PyDotTestParser
-    parsed_content = 'stdout'
-    options_def = PYVERSIONS_OPTIONS.copy()
-
-    def get_command(self, command, python):
-        # XXX coverage
-        return ['py.test', '--exec=%s' % python, '--nomagic', '--tb=no'] + command
-
-register('checker', PyDotTestChecker)
-
-
-class PyLintChecker(BaseChecker):
-    """check that the python package as a decent pylint evaluation
-    """
-    need_preprocessor = 'install'
-    id = 'pylint'
-    options_def = {
-        'pylintrc': {
-            'help': ('path to a pylint configuration file.'),
-            },
-        'pylint.threshold': {
-            'type': 'int', 'default': 7,
-            'help': ('integer between 1 and 10 telling expected pylint note to '
-                     'pass this check. Default to 7.'),
-         },
-        'pylint.show_categories': {
-            'type': 'csv', 'default': ['E', 'F'],
-            'help': ('comma separated list of pylint message categories to add to '
-                     'reports. Default to error (E) and failure (F).'),
-         },
-        'pylint.additional_builtins': {
-            'type': 'csv',
-            'help': ('comma separated list of additional builtins to give to '
-                     'pylint.'),
-            },
-        'pylint.disable': {
-            'type': 'csv',
-            'help': ('comma separated list of pylint message id that should be '
-                     'ignored.'),
-            },
-        'pylint.ignore': {
-            'type': 'csv',
-            'help': 'comma separated list of files or directories to ignore',
-            },
-        }
-
-    def version_info(self):
-        self.record_version_info('pylint', pylint_version)
-
-    def do_check(self, test):
-        """run the checker against <path> (usually a directory)"""
-        # XXX should consider python version
-        threshold = self.options.get('pylint.threshold')
-        pylintrc_path = self.options.get('pylintrc')
-        linter = PyLinter(pylintrc=pylintrc_path)
-        # register checkers
-        checkers.initialize(linter)
-        # load configuration
-        package_wd_path = test.project_path()
-        if exists(join(package_wd_path, 'pylintrc')):
-            linter.load_file_configuration(join(package_wd_path, 'pylintrc'))
-        else:
-            linter.load_file_configuration()
-        linter.set_option('persistent', False)
-        linter.set_option('reports', 0, action='store')
-        linter.quiet = 1
-        # set file or dir to ignore
-        for option in ('ignore', 'additional_builtins', 'disable'):
-            value = self.options.get('pylint.' + option)
-            if value is not None:
-                linter.global_set_option(option.replace('_', '-'), ','.join(value))
-        # message categories to record
-        categories = self.options.get('pylint.show_categories')
-        linter.set_reporter(MyLintReporter(self.writer, test.tmpdir, categories))
-        # run pylint
-        linter.check(pyinstall_path(test))
-        try:
-            note = eval(linter.config.evaluation, {}, linter.stats)
-            self.writer.raw('pylint.evaluation', '%.2f' % note, 'result')
-        except ZeroDivisionError:
-            self.writer.raw('pylint.evaluation', '0', 'result')
-            note = 0
-        except RESOURCE_LIMIT_EXCEPTION:
-            raise
-        except Exception:
-            self.writer.error('Error while processing pylint evaluation',
-                              path=test.project_path(subpath=True), tb=True)
-            note = 0
-        self.writer.raw('statements', '%i' % linter.stats['statement'], 'result')
-        if note < threshold:
-            return FAILURE
-        return SUCCESS
-
-try:
-    from pylint import checkers
-    from pylint.lint import PyLinter
-    from pylint.__pkginfo__ import version as pylint_version
-    from pylint.interfaces import IReporter
-    from pylint.reporters import BaseReporter
-    register('checker', PyLintChecker)
-
-    class MyLintReporter(BaseReporter):
-        """a partial pylint writer (implements only the message method, not
-        methods necessary to display layouts
-        """
-        __implements__ = IReporter
-
-        def __init__(self, writer, basepath, categories):
-            self.writer = writer
-            self.categories = set(categories)
-            self._to_remove = len(basepath) + 1 # +1 for the leading "/"
-
-        def add_message(self, msg_id, location, msg):
-            """ manage message of different type and in the context of path """
-            if not msg_id[0] in self.categories:
-                return
-            path, line = location[0], location[-1]
-            path = path[self._to_remove:]
-            if msg_id[0] == 'I':
-                self.writer.info(msg, path=path, line=line)
-            elif msg_id[0]  == 'E':
-                self.writer.error(msg, path=path, line=line)
-            elif msg_id[0] == 'F':
-                self.writer.fatal(msg, path=path, line=line)
-            else: # msg_id[0] in ('R', 'C', 'W')
-                self.writer.warning(msg, path=path, line=line)
-
-        def display_results(self, layout):
-            pass
-except ImportError, e:
-    warn("unable to import pylint. Pylint checker disabled : %s" % e)
-
-
-class PyCoverageChecker(BaseChecker):
-    """retrieve the tests coverage data
-
-    The coverage data are coming from the pyunit checker with the "pycoverage"
-    configuration variable
-
-    When devtools is available, test will be launched in a coverage mode. This
-    test will gather coverage information, and will succeed if the test coverage
-    is superior to a given threshold. *This checker must be executed after the
-    python_unittest checker.
-    """
-    id = 'pycoverage'
-    options_def = {
-        'coverage_threshold': {
-            'type': 'int', 'default': 80,
-            'help': ('integer between 1 and 100 telling expected percent coverage '
-                     'to pass this check. Default to 80.\n'
-                     'PARTIAL returned when cover rate between threshold and threshold / 2.\n'
-                     'ERROR returned when cover rate under threshold / 2'),
-        },
-        'coverage_data': {
-            'required': True,
-            'help': 'collect coverage data file',
-        },
-    }
-
-    def version_info(self):
-        if coverage:
-            version = getoutput('%s --version' % COVERAGE_CMD).strip()
-            self.record_version_info('python-coverage', version)
-
-    def do_check(self, test):
-        """run the checker against <path> (usually a directory)"""
-        self.threshold = float(self.options.get('coverage_threshold')) / 100
-        coverage_data = self.options.get('coverage_data')
-        if coverage_data == None or not exists(coverage_data):
-            self.writer.fatal('no coverage information', path=coverage_data)
-            return NODATA
-        line_rate, branch_rate = self._get_cover_info(test)
-        # in case of error during coverage reporting
-        if line_rate is None:
-            return ERROR
-        # global summary
-        self.writer.raw('cover-line-rate', '%.3f' % line_rate, 'result')
-        self.writer.raw('cover-branch-rate', '%.3f' % branch_rate, 'result')
-        if line_rate < self.threshold:
-            return FAILURE
-        return SUCCESS
-
-    def _get_log_method(self, pc_cover):
-        if pc_cover < (self.threshold / 2):
-            writer = self.writer.error
-        elif pc_cover < self.threshold:
-            writer = self.writer.warning
-        else:
-            writer = self.writer.info
-        return writer
-
-    def _get_cover_info(self, test):
-        if coverage is None:
-            raise Exception('install python-coverage')
-        covertool = coverage.coverage()
-        covertool.use_cache(self.options.get('coverage_data'))
-        covertool.load()
-        try:
-            report_file = join(test.project_path(), "coverage.xml")
-            covertool.xml_report(outfile=report_file, ignore_errors=True)
-            report = etree.parse(report_file).getroot()
-            pc_cover = float(report.attrib.get('line-rate'))
-            br_rate  = float(report.attrib.get('branch-rate'))
-            # format of the xml_report file is compatible with Cobertura
-            # <http://cobertura.sourceforge.net/>
-            #
-            # FIXME missing stats: <stat> / <miss>
-            # FIXME p_name construction (arbitrary slice)
-            for package in report.iter("package"):
-                p_name = package.attrib.get('name')
-                p_name = ".".join(p_name.split(".")[6:])
-                # class *are* files in Coberture (from java world)
-                for cls in package.iter("class"):
-                    c_name = ".".join([p_name, cls.attrib.get('name')])
-                    c_pc_cover = float(cls.attrib.get('line-rate'))
-                    c_br_rate  = float(cls.attrib.get('branch-rate'))
-                    logger = self._get_log_method(c_pc_cover)
-                    logger("line rate: %3.0f %% / branch rate: %3.0f %%"
-                           % (c_pc_cover*100, c_br_rate*100), path=c_name)
-        except Exception, err:
-            pc_cover = br_rate = None
-            self.writer.fatal(err, tb=True)
-        finally:
-            return (pc_cover, br_rate)
-
-if coverage is not None:
-    register('checker', PyCoverageChecker)
-
-
-class PyCheckerOutputParser(SimpleOutputParser):
-    non_zero_status_code = FAILURE
-    def parse_line(self, line):
-        try:
-            path, line, msg = line.split(':')
-            self.writer.error(msg, path=path, line=line)
-            self.status = FAILURE
-        except ValueError:
-            self.unparsed.append(line)
-
-class PyCheckerChecker(BaseChecker):
-    """check that unit tests of a python package succeed
-
-    spawn unittest and parse output (expect a standard TextTestRunner)
-    """
-    id = 'pychecker'
-    need_preprocessor = 'install'
-
-    def do_check(self, test):
-        """run the checker against <path> (usually a directory)"""
-        command = ['pychecker', '-Qqe', 'Style']
-        command += get_module_files(pyinstall_path(test))
-        return ParsedCommand(self.writer, command, parsercls=PyCheckerOutputParser).run()
-
-    def version_info(self):
-        self.record_version_info('pychecker', getoutput("pychecker --version").strip())
-
-register('checker', PyCheckerChecker)
--- a/_apycotlib/checkers/scenarios.py	Fri Jan 17 18:32:07 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-import os
-from commands import getstatusoutput
-from apycotlib import SUCCESS, FAILURE, ERROR
-from apycotlib import register
-
-from checkers.apycot import AbstractFilteredFileChecker
-
-class ScriptRunner(AbstractFilteredFileChecker):
-    """
-    run files accepted by the filter
-    """
-    id = 'script_runner'
-    def do_check(self, test):
-        if self.options.get('filename_filter') is not None:
-            self.filename_filter = self.options.get('filename_filter')
-        super(ScriptRunner, self).do_check(test)
-
-    def check_file(self, filepath):
-        try:
-            self.writer.debug("running : " + filepath, path=filepath)
-            status, out = getstatusoutput(filepath)
-            if status != 0:
-                self.writer.error(out, path=filepath)
-                return FAILURE
-            self.writer.info(out, path=filepath)
-            return SUCCESS
-        except Exception, error:
-            self.writer.error(error.msg, path=filepath)
-            return ERROR
-
-register('checker', ScriptRunner)
-
-
--- a/_apycotlib/preprocessors/__init__.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/_apycotlib/preprocessors/__init__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -1,14 +0,0 @@
-"""preprocessors packages"""
-
-__docformat__ = "restructuredtext en"
-
-from apycotlib import ApycotObject
-
-class BasePreProcessor(ApycotObject):
-    """an abstract class providing some common utilities for preprocessors
-    """
-    __type__ = 'preprocessor'
-
-    def run(self, test, path):
-        """Run preprocessor against source in <path> in <test> context"""
-        raise NotImplementedError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/preprocessors/apycot/__init__.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,14 @@
+"""preprocessors packages"""
+
+__docformat__ = "restructuredtext en"
+
+from apycotlib import ApycotObject
+
+class BasePreProcessor(ApycotObject):
+    """an abstract class providing some common utilities for preprocessors
+    """
+    __type__ = 'preprocessor'
+
+    def run(self, test, path):
+        """Run preprocessor against source in <path> in <test> context"""
+        raise NotImplementedError()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/_apycotlib/preprocessors/apycot/distutils.py	Tue Apr 08 15:28:12 2014 +0200
@@ -0,0 +1,131 @@
+"""installation preprocessor using distutils setup.py
+
+:organization: Logilab
+:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: General Public License version 2 - http://www.gnu.org/licenses
+"""
+from __future__ import with_statement
+
+__docformat__ = "restructuredtext en"
+
+import os
+import shutil
+import sys
+from os.path import join, exists, abspath, dirname
+from glob import glob
+
+from logilab.common.textutils import splitstrip
+from logilab.common.decorators import cached
+
+from apycotlib import register, SetupException
+from apycotlib import Command
+
+from preprocessors.apycot import BasePreProcessor
+
+def pyversion_available(python):
+    return not os.system('%s -V 2>/dev/null' % python)
+
+@cached
+def pyversions(test):
+    config = test.apycot_config()
+    tested_pyversions = config.get('tested_python_versions')
+    if tested_pyversions:
+        pyversions = set(splitstrip(tested_pyversions))
+    elif config.get('use_pkginfo_python_versions'):
+        from logilab.devtools.lib.pkginfo import PackageInfo
+        try:
+            pkginfodir = dirname(test.environ['pkginfo'])
+        except KeyError:
+            pkginfodir = test.project_path()
+        try:
+            pkginfo = PackageInfo(directory=pkginfodir)
+            pyversions = set(pkginfo.pyversions)
+        except (NameError, ImportError):
+            pyversions = set()
+        ignored_pyversions = config.get('ignored_python_versions')
+        if ignored_pyversions:
+            ignored_pyversions = set(ignored_pyversions)
+            ignored_pyversions = pyversions.intersection(
+                ignored_pyversions)
+            if ignored_pyversions:
+                for py_ver in ignored_pyversions:
+                    test.writer.debug('python version %s ignored', py_ver)
+                pyversions.difference_update(ignored_pyversions)
+    else:
+        pyversions = None
+    if pyversions:
+        pyversions_ = []
+        for pyver in pyversions:
+            python = 'python%s' % pyver
+            if not pyversion_available(python):
+                test.writer.error(
+                    'config asked for %s, but it\'s not available', pyver)
+            else:
+                pyversions_.append(python)
+        pyversions = pyversions_
+    else:
+        pyversions = ['python%s.%s' % sys.version_info[:2]]
+    return pyversions
+
+INSTALL_PREFIX = {}
+
+class DistutilsProcessor(BasePreProcessor):
+    """python setup.py pre-processor
+
+       Use a distutils'setup.py script to install a Python package. The
+       setup.py should provide an "install" function which run the setup and
+       return a "dist" object (i.e. the object return by the distutils.setup
+       function). This preprocessor may modify the PATH and PYTHONPATH
+       environment variables.
+    """
+    id = 'python_setup'
+    _python_path_set = None
+
+    options_def = {
+        'verbose': {
+            'type': 'int', 'default': False,
+            'help': 'set verbose mode'
+            },
+        }
+
+    # PreProcessor interface ##################################################
+
+    def run(self, test, path=None):
+        """run the distutils setup.py install method on a path if
+        the path is not yet installed
+        """
+        if path is None:
+            path = test.project_path()
+        if not DistutilsProcessor._python_path_set:
+            py_lib_dir = join(test.tmpdir, 'local', 'lib', 'python')
+            # setuptools need this directory to exists
+            if not exists(py_lib_dir):
+                os.makedirs(py_lib_dir)
+            test.update_env(path, 'PYTHONPATH', py_lib_dir, os.pathsep)
+            test.update_env(path, 'PATH', join(test.tmpdir, 'local', 'bin'),
+                            os.pathsep)
+            DistutilsProcessor._python_path_set = py_lib_dir
+            sitecustomize = open(join(py_lib_dir, 'sitecustomize.py'), 'w')
+            sitecustomize.write('''import sys, os.path as osp
+sys.path.insert(0, osp.join(%r, "python%%s.%%s" %% sys.version_info[:2], "site-packages"))
+''' % join(test.tmpdir, 'local', 'lib'))
+            sitecustomize.close()
+        # cache to avoid multiple installation of the same module
+        if path in INSTALL_PREFIX:
+            return
+        if not exists(join(path, 'setup.py')):
+            raise SetupException('No file %s' % abspath(join(path, 'setup.py')))
+        python = pyversions(test)[0]
+        INSTALL_PREFIX[path] = join(test.tmpdir, 'local', 'lib', 'python')
+        cmdargs = [python, 'setup.py', 'install', '--home',
+                   join(test.tmpdir, 'local')]
+        if not self.options.get('verbose'):
+            cmdargs.append('--quiet')
+        cmd = Command(self.writer, cmdargs, raises=True, cwd=path)
+        cmd.run()
+        if exists(join(path, 'build')):
+            shutil.rmtree(join(path, 'build')) # remove the build directory
+
+
+register('preprocessor', DistutilsProcessor)
--- a/_apycotlib/preprocessors/distutils.py	Fri Jan 17 18:32:07 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-"""installation preprocessor using distutils setup.py
-
-:organization: Logilab
-:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: General Public License version 2 - http://www.gnu.org/licenses
-"""
-from __future__ import with_statement
-
-__docformat__ = "restructuredtext en"
-
-import os
-import shutil
-import sys
-from os.path import join, exists, abspath, dirname
-from glob import glob
-
-from logilab.common.textutils import splitstrip
-from logilab.common.decorators import cached
-
-from apycotlib import register, SetupException
-from apycotlib import Command
-
-from preprocessors.apycot import BasePreProcessor
-
-def pyversion_available(python):
-    return not os.system('%s -V 2>/dev/null' % python)
-
-@cached
-def pyversions(test):
-    config = test.apycot_config()
-    tested_pyversions = config.get('tested_python_versions')
-    if tested_pyversions:
-        pyversions = set(splitstrip(tested_pyversions))
-    elif config.get('use_pkginfo_python_versions'):
-        from logilab.devtools.lib.pkginfo import PackageInfo
-        try:
-            pkginfodir = dirname(test.environ['pkginfo'])
-        except KeyError:
-            pkginfodir = test.project_path()
-        try:
-            pkginfo = PackageInfo(directory=pkginfodir)
-            pyversions = set(pkginfo.pyversions)
-        except (NameError, ImportError):
-            pyversions = set()
-        ignored_pyversions = config.get('ignored_python_versions')
-        if ignored_pyversions:
-            ignored_pyversions = set(ignored_pyversions)
-            ignored_pyversions = pyversions.intersection(
-                ignored_pyversions)
-            if ignored_pyversions:
-                for py_ver in ignored_pyversions:
-                    test.writer.debug('python version %s ignored', py_ver)
-                pyversions.difference_update(ignored_pyversions)
-    else:
-        pyversions = None
-    if pyversions:
-        pyversions_ = []
-        for pyver in pyversions:
-            python = 'python%s' % pyver
-            if not pyversion_available(python):
-                test.writer.error(
-                    'config asked for %s, but it\'s not available', pyver)
-            else:
-                pyversions_.append(python)
-        pyversions = pyversions_
-    else:
-        pyversions = ['python%s.%s' % sys.version_info[:2]]
-    return pyversions
-
-INSTALL_PREFIX = {}
-
-class DistutilsProcessor(BasePreProcessor):
-    """python setup.py pre-processor
-
-       Use a distutils'setup.py script to install a Python package. The
-       setup.py should provide an "install" function which run the setup and
-       return a "dist" object (i.e. the object return by the distutils.setup
-       function). This preprocessor may modify the PATH and PYTHONPATH
-       environment variables.
-    """
-    id = 'python_setup'
-    _python_path_set = None
-
-    options_def = {
-        'verbose': {
-            'type': 'int', 'default': False,
-            'help': 'set verbose mode'
-            },
-        }
-
-    # PreProcessor interface ##################################################
-
-    def run(self, test, path=None):
-        """run the distutils setup.py install method on a path if
-        the path is not yet installed
-        """
-        if path is None:
-            path = test.project_path()
-        if not DistutilsProcessor._python_path_set:
-            py_lib_dir = join(test.tmpdir, 'local', 'lib', 'python')
-            # setuptools need this directory to exists
-            if not exists(py_lib_dir):
-                os.makedirs(py_lib_dir)
-            test.update_env(path, 'PYTHONPATH', py_lib_dir, os.pathsep)
-            test.update_env(path, 'PATH', join(test.tmpdir, 'local', 'bin'),
-                            os.pathsep)
-            DistutilsProcessor._python_path_set = py_lib_dir
-            sitecustomize = open(join(py_lib_dir, 'sitecustomize.py'), 'w')
-            sitecustomize.write('''import sys, os.path as osp
-sys.path.insert(0, osp.join(%r, "python%%s.%%s" %% sys.version_info[:2], "site-packages"))
-''' % join(test.tmpdir, 'local', 'lib'))
-            sitecustomize.close()
-        # cache to avoid multiple installation of the same module
-        if path in INSTALL_PREFIX:
-            return
-        if not exists(join(path, 'setup.py')):
-            raise SetupException('No file %s' % abspath(join(path, 'setup.py')))
-        python = pyversions(test)[0]
-        INSTALL_PREFIX[path] = join(test.tmpdir, 'local', 'lib', 'python')
-        cmdargs = [python, 'setup.py', 'install', '--home',
-                   join(test.tmpdir, 'local')]
-        if not self.options.get('verbose'):
-            cmdargs.append('--quiet')
-        cmd = Command(self.writer, cmdargs, raises=True, cwd=path)
-        cmd.run()
-        if exists(join(path, 'build')):
-            shutil.rmtree(join(path, 'build')) # remove the build directory
-
-
-register('preprocessor', DistutilsProcessor)
--- a/debian/narval-apycot.install	Fri Jan 17 18:32:07 2014 +0100
+++ b/debian/narval-apycot.install	Tue Apr 08 15:28:12 2014 +0200
@@ -1,4 +1,4 @@
-usr/share/narval/checkers
-usr/share/narval/preprocessors
+usr/share/narval/checkers/apycot
+usr/share/narval/preprocessors/apycot
 usr/share/narval/data
 usr/lib/python*
--- a/debian/rules	Fri Jan 17 18:32:07 2014 +0100
+++ b/debian/rules	Tue Apr 08 15:28:12 2014 +0200
@@ -31,13 +31,13 @@
 # Build architecture-independent files here.
 binary-indep: build install
 	dh_testdir
-	dh_testroot
-	dh_install -i --list-missing
-	dh_pysupport -i /usr/share/cubicweb /usr/share/narval
+	dh_testroot -i
+	dh_install -i 
 	dh_installchangelogs -i
 	dh_installexamples -i
 	dh_installdocs -i -A README
 	dh_installman -i
+	dh_pysupport -i /usr/share/cubicweb /usr/share/narval
 	dh_link -i
 	dh_compress -i -X.py -X.ini -X.xml -Xtest
 	dh_fixperms -i
--- a/test/unittest_checkers_jslint.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/test/unittest_checkers_jslint.py	Tue Apr 08 15:28:12 2014 +0200
@@ -3,7 +3,8 @@
 from unittest_checkers import FileCheckerTest, WRITER
 
 try:
-    from apycotlib.checkers.jslint import JsLintChecker, JsLintParser
+    import cubes.apycot
+    from checkers.apycot.jslint import JsLintChecker, JsLintParser
 
     def load_tests(loader, tests, pattern):
         testsuite = TestSuite()
--- a/test/unittest_checkers_pyunit.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/test/unittest_checkers_pyunit.py	Tue Apr 08 15:28:12 2014 +0200
@@ -10,7 +10,7 @@
 from cubes.apycot.testutils import MockCheckWriter, input_path
 
 from apycotlib import SUCCESS, FAILURE, ERROR, PARTIAL, NODATA
-from apycotlib.checkers import python
+from checkers.apycot import python
 
 def _test_cmd(self, cmd, status, success=0, failures=0, errors=0, skipped=0):
 
--- a/test/unittest_parser.py	Fri Jan 17 18:32:07 2014 +0100
+++ b/test/unittest_parser.py	Tue Apr 08 15:28:12 2014 +0200
@@ -2,11 +2,11 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubes.apycot import testutils # import this first
+import cubes.apycot # import this first
 
 from apycotlib import SimpleOutputParser
 from apycotlib.writer import AbstractLogWriter
-from apycotlib.checkers.jslint import JsLintParser
+from checkers.apycot.jslint import JsLintParser
 
 
 class SimpleLogWriter(AbstractLogWriter):