From 2861a3f05af2a284173744c2715015d8380b22ee Mon Sep 17 00:00:00 2001
From: Sebastien Robin <seb@nexedi.com>
Date: Mon, 29 Oct 2012 16:54:50 +0100
Subject: [PATCH] erp5testnode: more cleanup and add skeleton of test

- various cleanup and split of code to make it more testable
- start writing some tests
---
 erp5/util/testnode/SlapOSControler.py |  75 ++++++++------
 erp5/util/testnode/testnode.py        | 139 ++++++++++++++------------
 erp5/util/tests/testERP5TestNode.py   |  50 +++++++++
 setup.py                              |   3 +-
 4 files changed, 174 insertions(+), 93 deletions(-)
 create mode 100644 erp5/util/tests/testERP5TestNode.py

diff --git a/erp5/util/testnode/SlapOSControler.py b/erp5/util/testnode/SlapOSControler.py
index c347fa9205..d2aac18bed 100644
--- a/erp5/util/testnode/SlapOSControler.py
+++ b/erp5/util/testnode/SlapOSControler.py
@@ -25,6 +25,7 @@
 #
 ##############################################################################
 import os
+import pkg_resources
 import slapos.slap
 import subprocess
 import time
@@ -35,25 +36,43 @@ import glob
 MAX_PARTIONS = 10
 MAX_SR_RETRIES = 3
 
+def createFolder(folder):
+  if not(os.path.exists(folder)):
+    os.mkdir(folder)
+
 class SlapOSControler(object):
 
-  def __init__(self, config, log,
-      slapproxy_log=None, process_manager=None, reset_software=False):
+  def __init__(self, working_directory, config, log, slapproxy_log=None,
+      process_manager=None, reset_software=False, software_path_list=None):
+    self.software_path_list = software_path_list
     log('SlapOSControler, initialize, reset_software: %r' % reset_software)
     self.log = log
     self.config = config
     self.process_manager = process_manager
+    self.software_root = os.path.join(working_directory, 'soft')
+    self.instance_root = os.path.join(working_directory, 'inst')
+    self.slapos_config = os.path.join(working_directory, 'slapos.cfg')
+    self.proxy_database = os.path.join(working_directory, 'proxy.db')
+    slapos_config_dict = config.copy()
+    slapos_config_dict.update(software_root=self.software_root,
+                              instance_root=self.instance_root,
+                              proxy_database=self.proxy_database)
+    open(self.slapos_config, 'w').write(pkg_resources.resource_string(
+         'erp5.util.testnode', 'template/slapos.cfg.in') %
+           slapos_config_dict)
+    createFolder(self.software_root)
+    createFolder(self.instance_root)
     # By erasing everything, we make sure that we are able to "update"
     # existing profiles. This is quite dirty way to do updates...
-    if os.path.exists(config['proxy_database']):
-      os.unlink(config['proxy_database'])
+    if os.path.exists(self.proxy_database):
+      os.unlink(self.proxy_database)
     kwargs = dict(close_fds=True, preexec_fn=os.setsid)
     if slapproxy_log is not None:
       slapproxy_log_fp = open(slapproxy_log, 'w')
       kwargs['stdout'] = slapproxy_log_fp
       kwargs['stderr'] = slapproxy_log_fp
     proxy = subprocess.Popen([config['slapproxy_binary'],
-      config['slapos_config']], **kwargs)
+      self.slapos_config], **kwargs)
     process_manager.process_pid_set.add(proxy.pid)
     # XXX: dirty, giving some time for proxy to being able to accept
     # connections
@@ -62,8 +81,6 @@ class SlapOSControler(object):
     self.slap = slap
     self.slap.initializeConnection(config['master_url'])
     # register software profile
-    self.software_path_list = config.get("software_list", [])
-    self.software_path_list.append(config['custom_profile_path'])
     for path in self.software_path_list:
       slap.registerSupply().supply(
           path,
@@ -71,13 +88,12 @@ class SlapOSControler(object):
     computer = slap.registerComputer(config['computer_id'])
     # Reset all previously generated software if needed
     if reset_software:
-      software_root = config['software_root']
-      log('SlapOSControler : GOING TO RESET ALL SOFTWARE : %r' % (software_root,))
-      if os.path.exists(software_root):
-        shutil.rmtree(software_root)
-      os.mkdir(software_root)
-      os.chmod(software_root, 0750)
-    instance_root = config['instance_root']
+      log('SlapOSControler : GOING TO RESET ALL SOFTWARE : %r' % (self.software_root,))
+      if os.path.exists(self.software_root):
+        shutil.rmtree(self.software_root)
+      os.mkdir(self.software_root)
+      os.chmod(self.software_root, 0750)
+    instance_root = self.instance_root
     if os.path.exists(instance_root):
       # delete old paritions which may exists in order to not get its data
       # (ex. MySQL db content) from previous testnode's runs
@@ -96,20 +112,19 @@ class SlapOSControler(object):
         os.mkdir(partition_path)
       os.chmod(partition_path, 0750)
       computer.updateConfiguration(xml_marshaller.xml_marshaller.dumps({
-                                                    'address': config['ipv4_address'],
-                                                    'instance_root': instance_root,
-                                                    'netmask': '255.255.255.255',
-                                                    'partition_list': [{'address_list': [{'addr': config['ipv4_address'],
-                                                                        'netmask': '255.255.255.255'},
-                                                                       {'addr': config['ipv6_address'],
-                                                                        'netmask': 'ffff:ffff:ffff::'},],
-                                                    'path': partition_path,
-                                                    'reference': partition_reference,
-                                                    'tap': {'name': partition_reference},
-                                                    }
-                                                    ],
-                                    'reference': config['computer_id'],
-                                    'software_root': config['software_root']}))
+           'address': config['ipv4_address'],
+           'instance_root': instance_root,
+           'netmask': '255.255.255.255',
+           'partition_list': [
+             {'address_list': [{'addr': config['ipv4_address'],
+                               'netmask': '255.255.255.255'},
+                              {'addr': config['ipv6_address'],
+                               'netmask': 'ffff:ffff:ffff::'},],
+              'path': partition_path,
+              'reference': partition_reference,
+              'tap': {'name': partition_reference},}],
+           'reference': config['computer_id'],
+           'software_root': self.software_root}))
 
   def spawn(self, *args, **kw):
     return self.process_manager.spawn(*args, **kw)
@@ -123,7 +138,7 @@ class SlapOSControler(object):
     # so be tolerant and run it a few times before giving up
     for runs in range(0, MAX_SR_RETRIES):
       status_dict = self.spawn(config['slapgrid_software_binary'], '-v', '-c',
-                 config['slapos_config'], raise_error_if_fail=False,
+                 self.slapos_config, raise_error_if_fail=False,
                  log_prefix='slapgrid_sr', get_output=False)
       if status_dict['status_code'] == 0:
         break
@@ -150,7 +165,7 @@ class SlapOSControler(object):
     sleep_time = 0
     for runs in range(0, MAX_PARTIONS):
       status_dict = self.spawn(config['slapgrid_partition_binary'], '-v', '-c',
-                 config['slapos_config'], raise_error_if_fail=False,
+                 self.slapos_config, raise_error_if_fail=False,
                  log_prefix='slapgrid_cp', get_output=False)
       self.log('slapgrid_cp status_dict : %r' % (status_dict,))
       if status_dict['status_code'] in (0,):
diff --git a/erp5/util/testnode/testnode.py b/erp5/util/testnode/testnode.py
index 8e828e17ae..d0da2bf995 100644
--- a/erp5/util/testnode/testnode.py
+++ b/erp5/util/testnode/testnode.py
@@ -34,7 +34,6 @@ import SlapOSControler
 import json
 import time
 import shutil
-import pkg_resources
 from ProcessManager import SubprocessError, ProcessManager, CancellationError
 from subprocess import CalledProcessError
 from Updater import Updater
@@ -51,12 +50,19 @@ class DummyLogger(object):
       'critical', 'fatal'):
        setattr(self, name, func)
 
-class NodeTestSuite(object):
+class SlapOSInstance(object):
 
-  def __init__(self, reference=None):
-    self.reference = reference
+  def __init__(self, working_directory):
     self.retry_software_count = 0
     self.retry = False
+    self.working_directory = working_directory
+
+class NodeTestSuite(SlapOSInstance):
+
+  def __init__(self, reference, working_directory):
+    super(NodeTestSuite, self).__init__(working_directory)
+    self.reference = reference
+    self.working_directory = os.path.join(working_directory, reference)
 
   def edit(self, **kw):
     self.__dict__.update(**kw)
@@ -68,6 +74,10 @@ class TestNode(object):
     self.config = config
     self.process_manager = ProcessManager(log)
     self.node_test_suite_dict = {}
+    # hack until slapos.cookbook is updated
+    if config['working_directory'].endswith("slapos/"):
+      config['working_directory'] = config[
+        'working_directory'][:-(len("slapos/"))]
 
   def checkOldTestSuite(self,test_suite_data):
     config = self.config
@@ -87,44 +97,24 @@ class TestNode(object):
     node_test_suite = self.node_test_suite_dict.get(reference)
     if node_test_suite is None:
       node_test_suite = self.node_test_suite_dict[reference] = NodeTestSuite(
-                             reference=reference)
+                             reference, working_directory)
     return node_test_suite
 
   def delNodeTestSuite(self, reference):
     if self.node_test_suite_dict.has_key(reference):
       self.node_test_suite_dict.pop(reference)
 
-  def updateConfigForTestSuite(self, test_suite):
+  def updateConfigForTestSuite(self, test_suite, node_test_suite):
     config = self.config
-    node_test_suite = self.getNodeTestSuite(test_suite["test_suite_reference"])
     node_test_suite.edit(project_title=test_suite["project_title"],
                          test_suite=test_suite["test_suite"],
                          test_suite_title=test_suite["test_suite_title"])
-    try:
-      config["additional_bt5_repository_id"] = test_suite["additional-bt5-repository-id"]
-    except KeyError:
-      pass
     config["vcs_repository_list"] = test_suite["vcs_repository_list"]
     config['working_directory'] = os.path.join(config['slapos_directory'],
                                            node_test_suite.reference)
-    if not(os.path.exists(config['working_directory'])):
-      os.mkdir(config['working_directory'])
-    config['instance_root'] = os.path.join(config['working_directory'],
-                                           'inst')
-    if not(os.path.exists(config['instance_root'])):
-      os.mkdir(config['instance_root'])
-    config['software_root'] = os.path.join(config['working_directory'],
-                                           'soft')
-    if not(os.path.exists(config['software_root'])):
-      os.mkdir(config['software_root'])
-    config['proxy_database'] = os.path.join(config['working_directory'],
-                                                'proxy.db')
+    SlapOSControler.createFolder(config['working_directory'])
     custom_profile_path = os.path.join(config['working_directory'], 'software.cfg')
     config['custom_profile_path'] = custom_profile_path
-    config['slapos_config'] = os.path.join(config['working_directory'],
-                                           'slapos.cfg')
-    open(config['slapos_config'], 'w').write(pkg_resources.resource_string(
-    'erp5.util.testnode', 'template/slapos.cfg.in') % config)
 
   def constructProfile(self):
     vcs_repository_list = self.config['vcs_repository_list']
@@ -182,7 +172,6 @@ branch = %(branch)s
     full_revision_list = []
     config = self.config
     log = self.log
-    process_manager = self.process_manager
     vcs_repository_list = self.config['vcs_repository_list']
     for vcs_repository in vcs_repository_list:
       repository_path = vcs_repository['repository_path']
@@ -196,7 +185,7 @@ branch = %(branch)s
         log(subprocess.check_output(parameter_list, stderr=subprocess.STDOUT))
       # Make sure we have local repository
       updater = Updater(repository_path, git_binary=config['git_binary'],
-         log=log, process_manager=process_manager)
+         log=log, process_manager=self.process_manager)
       updater.checkout()
       revision = "-".join(updater.getRevision())
       full_revision_list.append('%s=%s' % (repository_id, revision))
@@ -216,7 +205,6 @@ branch = %(branch)s
   def checkRevision(self, test_result, node_test_suite, vcs_repository_list):
     config = self.config
     log = self.log
-    process_manager = self.process_manager
     if node_test_suite.revision != test_result.revision:
      log('Disagreement on tested revision, checking out: %r' % (
           (node_test_suite.revision,test_result.revision),))
@@ -229,35 +217,55 @@ branch = %(branch)s
       log('  %s at %s' % (repository_path, node_test_suite.revision))
       updater = Updater(repository_path, git_binary=config['git_binary'],
                         revision=revision, log=log,
-                        process_manager=process_manager)
+                        process_manager=self.process_manager)
       updater.checkout()
       node_test_suite.revision = test_result.revision
 
-  def prepareSlapOSForTestSuite(self,node_test_suite):
-    config = self.config
-    log = self.log
-    process_manager = self.process_manager
-    slapproxy_log = os.path.join(config['log_directory'],
+  def _prepareSlapOS(self, working_directory, slapos_instance,
+          create_partition=1, software_path_list=None, **kw):
+    """
+    Launch slapos to build software and partitions
+    """
+    slapproxy_log = os.path.join(self.config['log_directory'],
                                   'slapproxy.log')
-    log('Configured slapproxy log to %r' % slapproxy_log)
-    retry_software_count = node_test_suite.retry_software_count
-    log('testnode, retry_software_count : %r' % retry_software_count)
-    slapos_controler = SlapOSControler.SlapOSControler(config,
-      log=log, slapproxy_log=slapproxy_log, process_manager=process_manager,
-      reset_software=(retry_software_count>0 and retry_software_count%10 == 0))
-    for method_name in ("runSoftwareRelease", "runComputerPartition",):
+    self.log('Configured slapproxy log to %r' % slapproxy_log)
+    reset_software = slapos_instance.retry_software_count > 10
+    self.log('testnode, retry_software_count : %r' % \
+             slapos_instance.retry_software_count)
+    slapos_controler = SlapOSControler.SlapOSControler(
+      working_directory, self.config, log=self.log, slapproxy_log=slapproxy_log,
+      process_manager=self.process_manager, reset_software=reset_software,
+      software_path_list=software_path_list)
+    method_list= ["runSoftwareRelease"]
+    if create_partition:
+      method_list.append("runComputerPartition")
+    for method_name in method_list:
       slapos_method = getattr(slapos_controler, method_name)
-      status_dict = slapos_method(config,
-                                  environment=config['environment'],
+      status_dict = slapos_method(self.config,
+                                  environment=self.config['environment'],
                                  )
       if status_dict['status_code'] != 0:
-         node_test_suite.retry = True
-         node_test_suite.retry_software_count += 1
+         slapos_instance.retry = True
+         slapos_instance.retry_software_count += 1
          raise SubprocessError(status_dict)
       else:
-         node_test_suite.retry_software_count = 0
+         slapos_instance.retry_software_count = 0
     return status_dict
 
+  def prepareSlapOSForTestNode(self, test_node_slapos):
+    """
+    We will build slapos software needed by the testnode itself,
+    like the building of selenium-runner by default
+    """
+    return self._prepareSlapOS(self.config['slapos_directory'],
+              test_node_slapos, create_partition=0,
+              software_path_list=self.config.get("software_list"))
+
+  def prepareSlapOSForTestSuite(self, node_test_suite):
+    return self._prepareSlapOS(node_test_suite.working_directory,
+              node_test_suite,
+              software_path_list=[self.config.get("custom_profile_path")])
+
   def _dealShebang(self,run_test_suite_path):
     line = open(run_test_suite_path, 'r').readline()
     invocation_list = []
@@ -265,10 +273,11 @@ branch = %(branch)s
       invocation_list = line[2:].split()
     return invocation_list
 
-  def runTestSuite(self, node_test_suite, portal_url):
+  def runTestSuite(self, node_test_suite, portal_url, slapos_controler):
     config = self.config
 
-    run_test_suite_path_list = glob.glob("%s/*/bin/runTestSuite" %config['instance_root'])
+    run_test_suite_path_list = glob.glob("%s/*/bin/runTestSuite" % \
+        slapos_controler.instance_root)
     if not len(run_test_suite_path_list):
       raise ValueError('No runTestSuite provided in installed partitions.')
     run_test_suite_path = run_test_suite_path_list[0]
@@ -281,10 +290,12 @@ branch = %(branch)s
                            '--test_suite_title', node_test_suite.test_suite_title,
                            '--node_quantity', config['node_quantity'],
                            '--master_url', portal_url])
-    firefox_bin_list = glob.glob("%s/*/parts/firefox/firefox-slapos" % config["software_root"])
+    firefox_bin_list = glob.glob("%s/*/parts/firefox/firefox-slapos" % \
+        config["slapos_directory"])
     if len(firefox_bin_list):
       invocation_list.extend(["--firefox_bin", firefox_bin_list[0]])
-    xvfb_bin_list = glob.glob("%s/*/parts/xserver/bin/Xvfb" % config["software_root"])
+    xvfb_bin_list = glob.glob("%s/*/parts/xserver/bin/Xvfb" % \
+        config["slapos_directory"])
     if len(xvfb_bin_list):
       invocation_list.extend(["--xvfb_bin", xvfb_bin_list[0]])
     bt5_path_list = config.get("bt5_path")
@@ -298,10 +309,9 @@ branch = %(branch)s
                           log_prefix='runTestSuite', get_output=False)
 
   def cleanUp(self,test_result):
-    process_manager = self.process_manager
     log = self.log
     log('Testnode.run, finally close')
-    process_manager.killPreviousRun()
+    self.process_manager.killPreviousRun()
     if test_result is not None:
       try:
         test_result.removeWatch(self.config['log_file'])
@@ -311,15 +321,17 @@ branch = %(branch)s
   def run(self):
     log = self.log
     config = self.config
-    process_manager = self.process_manager
     slapgrid = None
     previous_revision_dict = {}
     revision_dict = {}
     test_result = None
+    test_node_slapos = SlapOSInstance(self.config['slapos_directory'])
     try:
       while True:
         try:
+          remote_test_result_needs_cleanup = False
           begin = time.time()
+          self.prepareSlapOSForTestNode(test_node_slapos)
           portal_url = config['test_suite_master_url']
           portal = taskdistribution.TaskDistributionTool(portal_url, logger=DummyLogger(log))
           test_suite_portal = taskdistribution.TaskDistributor(portal_url, logger=DummyLogger(log))
@@ -330,15 +342,18 @@ branch = %(branch)s
           #Clean-up test suites
           self.checkOldTestSuite(test_suite_data)
           for test_suite in test_suite_data:
-            self.updateConfigForTestSuite(test_suite)
-            node_test_suite = self.getNodeTestSuite(test_suite["test_suite_reference"])
+            remote_test_result_needs_cleanup = False
+            node_test_suite = self.getNodeTestSuite(
+               test_suite["test_suite_reference"],
+               self.config['working_directory'])
+            self.updateConfigForTestSuite(test_suite, node_test_suite)
             run_software = True
-            self.process_manager.supervisord_pid_file = os.path.join(config['instance_root'], 'var', 'run',
-            'supervisord.pid')
+            self.process_manager.supervisord_pid_file = os.path.join(\
+                slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
             # Write our own software.cfg to use the local repository
             vcs_repository_list = self.constructProfile()
             # kill processes from previous loop if any
-            process_manager.killPreviousRun()
+            self.process_manager.killPreviousRun()
             self.getAndUpdateFullRevisionList(node_test_suite)
             # Make sure we have local repository
             test_result = portal.createTestResult(node_test_suite.revision, [],
@@ -356,7 +371,7 @@ branch = %(branch)s
               # as partitions can be of any kind we have and likely will never have
               # a reliable way to check if they are up or not ...
               time.sleep(20)
-              self.runTestSuite(node_test_suite,portal_url)
+              self.runTestSuite(node_test_suite,portal_url, slapos_controler)
               test_result.removeWatch(log_file_name)
         except (SubprocessError, CalledProcessError) as e:
           log("SubprocessError", exc_info=sys.exc_info())
@@ -376,7 +391,7 @@ branch = %(branch)s
           node_test_suite.retry_software_count += 1
         except CancellationError, e:
           log("CancellationError", exc_info=sys.exc_info())
-          process_manager.under_cancellation = False
+          self.process_manager.under_cancellation = False
           node_test_suite.retry = True
           continue
         except:
diff --git a/erp5/util/tests/testERP5TestNode.py b/erp5/util/tests/testERP5TestNode.py
new file mode 100644
index 0000000000..4a0611a2f1
--- /dev/null
+++ b/erp5/util/tests/testERP5TestNode.py
@@ -0,0 +1,50 @@
+from unittest import TestCase
+
+import sys
+sys.path[0:0] = [
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/slapos.cookbook-0.65-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/zc.recipe.egg-1.3.2-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/zc.buildout-1.6.0_dev_SlapOS_010-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/xml_marshaller-0.9.7-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/slapos.core-0.28.5-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/inotifyx-0.2.0-py2.7-linux-x86_64.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/setuptools-0.6c12dev_r88846-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/netaddr-0.7.10-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/develop-eggs/lxml-2.3.6-py2.7-linux-x86_64.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/PyXML-0.8.4-py2.7-linux-x86_64.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/zope.interface-4.0.1-py2.7-linux-x86_64.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/supervisor-3.0a12-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/netifaces-0.8-py2.7-linux-x86_64.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/Flask-0.9-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/meld3-0.6.8-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/Jinja2-2.6-py2.7.egg',
+    '/srv/slapgrid/slappart80/srv/runner/software/ba1e09f3364989dc92da955b64e72f8d/eggs/Werkzeug-0.8.3-py2.7.egg',
+    ]
+
+from erp5.util.testnode import TestNode
+import tempfile
+import shutil
+import os
+
+class ERP5TestNode(TestCase):
+
+  def setUp(self):
+    self._tempdir = tempfile.mkdtemp()
+    self.software_root = os.path.join(self._tempdir, 'software')
+    self.instance_root = os.path.join(self._tempdir, 'instance')
+
+  def tearDown(self):
+    shutil.rmtree(self._tempdir, True)
+
+  def test_01_GetDelNodeTestSuite(self):
+    test_node = TestNode(None, None)
+    node_test_suite = test_node.getNodeTestSuite('foo')
+    self.assertEquals(0, node_test_suite.retry_software_count)
+    node_test_suite.retry_software_count = 2
+    self.assertEquals(2, node_test_suite.retry_software_count)
+    node_test_suite = test_node.delNodeTestSuite('foo')
+    node_test_suite = test_node.getNodeTestSuite('foo')
+    self.assertEquals(0, node_test_suite.retry_software_count)
+
+  def test_01_GetNodeTestSuite(self):
+    pass
diff --git a/setup.py b/setup.py
index eaf8878b28..ebc7cb5182 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,8 @@ setup(name=name,
             'erp5.util.benchmark.report:generateReport [benchmark-report]',
           'web_checker_utility = erp5.util.webchecker:web_checker_utility'
         ],
-      }
+      },
+      test_suite='erp5.tests'
     )
 
 # cleanup garbage
-- 
2.30.9