diff --git a/erp5/tests/testERP5TestNode.py b/erp5/tests/testERP5TestNode.py
index edb81392f11f1f81d6c1cd4f9245ef0a74b07fe5..07b0716a1170c87d25f9daf6fe3025a669a9ec34 100644
--- a/erp5/tests/testERP5TestNode.py
+++ b/erp5/tests/testERP5TestNode.py
@@ -1,12 +1,16 @@
 from unittest import TestCase
 
 from erp5.util.testnode.testnode import TestNode
-from erp5.util.testnode.testnode import SlapOSInstance
+from erp5.util.testnode.NodeTestSuite import SlapOSInstance, NodeTestSuite
 from erp5.util.testnode.ProcessManager import ProcessManager, SubprocessError
 from erp5.util.testnode.Updater import Updater
 
+from erp5.util.testnode.SlapOSMasterCommunicator import SlapOSMasterCommunicator
 from erp5.util.testnode.SlapOSControler import SlapOSControler
+from erp5.util.testnode.UnitTestRunner import UnitTestRunner
+from erp5.util.testnode.ScalabilityTestRunner import ScalabilityTestRunner
 from erp5.util.testnode.SlapOSControler import createFolder
+
 from erp5.util.taskdistribution import TaskDistributor
 from erp5.util.taskdistribution import TaskDistributionTool
 from erp5.util.taskdistribution import TestResultProxy
@@ -25,6 +29,7 @@ class ERP5TestNode(TestCase):
     self._temp_dir = tempfile.mkdtemp()
     self.working_directory = os.path.join(self._temp_dir, 'testnode')
     self.slapos_directory = os.path.join(self._temp_dir, 'slapos')
+    self.software_directory = os.path.join(self._temp_dir, 'software_directory')
     self.test_suite_directory = os.path.join(self._temp_dir,'test_suite')
     self.environment = os.path.join(self._temp_dir,'environment')
     self.log_directory = os.path.join(self._temp_dir,'var/log/testnode')
@@ -36,6 +41,7 @@ class ERP5TestNode(TestCase):
     os.mkdir(self.working_directory)
     os.mkdir(self.slapos_directory)
     os.mkdir(self.test_suite_directory)
+    os.mkdir(self.software_directory)
     os.mkdir(self.environment)
     os.mkdir(self.system_temp_folder)
     os.makedirs(self.log_directory)
@@ -45,9 +51,17 @@ class ERP5TestNode(TestCase):
     os.mkdir(self.remote_repository2)
     def log(*args,**kw):
       for arg in args:
-        print "TESTNODE LOG : %r" % (arg,)
+        print "TESTNODE LOG : %r, %r" % (arg, kw)
     self.log = log
 
+  def returnGoodClassRunner(self, test_type):
+      if test_type == 'UnitTest':
+        return UnitTestRunner
+      elif test_type == 'ScalabilityTest':
+        return ScalabilityTestRunner
+      else:
+        raise NotImplementedError
+
   def tearDown(self):
     shutil.rmtree(self._temp_dir, True)
 
@@ -55,15 +69,23 @@ class ERP5TestNode(TestCase):
     # XXX how to get property the git path ?
     config = {}
     config["git_binary"] = "git"
-    config["slapos_directory"] = config["working_directory"] = self.working_directory
+    config["slapos_directory"] = self.slapos_directory
+    config["working_directory"] = self.working_directory
+    config["software_directory"] = self.software_directory
     config["node_quantity"] = 3
     config["test_suite_directory"] = self.test_suite_directory
     config["environment"] = self.environment
     config["log_directory"] = self.log_directory
     config["log_file"] = self.log_file
     config["test_suite_master_url"] = None
+    config["hateoas_slapos_master_url"] = None
     config["test_node_title"] = "Foo-Test-Node"
     config["system_temp_folder"] = self.system_temp_folder
+    config["computer_id"] = "COMP-TEST"
+    config["server_url"] = "http://foo.bar"
+    config["httpd_ip"] = "ff:ff:ff:ff:ff:ff:ff:ff"
+    config["httpd_software_access_port"] = "9080"
+    
     return TestNode(self.log, config)
 
   def getTestSuiteData(self, add_third_repository=False, reference="foo"):
@@ -91,6 +113,9 @@ class ERP5TestNode(TestCase):
 
   def updateNodeTestSuiteData(self, node_test_suite,
                               add_third_repository=False):
+    """
+    Update from zero/Regenerate the testsuite
+    """
     node_test_suite.edit(working_directory=self.working_directory,
        **self.getTestSuiteData(add_third_repository=add_third_repository)[0])
 
@@ -143,7 +168,7 @@ class ERP5TestNode(TestCase):
     #           ['4f1d14de1b04b4f878a442ee859791fa337bcf85', 'first_commit']]}
     return commit_dict
 
-  def test_01_getDelNodeTestSuite(self):
+  def test_01_getDelNodeTestSuite(self, my_test_type='UnitTest'):
     """
     We should be able to get/delete NodeTestSuite objects inside test_node
     """
@@ -156,7 +181,7 @@ class ERP5TestNode(TestCase):
     node_test_suite = test_node.getNodeTestSuite('foo')
     self.assertEquals(0, node_test_suite.retry_software_count)
 
-  def test_02_NodeTestSuiteWorkingDirectory(self):
+  def test_02_NodeTestSuiteWorkingDirectory(self, my_test_type='UnitTest'):
     """
     Make sure we extend the working path with the node_test_suite reference
     """
@@ -168,7 +193,7 @@ class ERP5TestNode(TestCase):
     self.assertEquals("%s/foo/test_suite" % self.working_directory,
                       node_test_suite.test_suite_directory)
 
-  def test_03_NodeTestSuiteCheckDataAfterEdit(self):
+  def test_03_NodeTestSuiteCheckDataAfterEdit(self, my_test_type='UnitTest'):
     """
     When a NodeTestSuite instance is edited, the method _checkData
     analyse properties and add new ones
@@ -184,18 +209,22 @@ class ERP5TestNode(TestCase):
                      "%s/rep1" % node_test_suite.working_directory]
     self.assertEquals(expected_list, repository_path_list)
 
-  def test_04_constructProfile(self):
+  def test_04_constructProfile(self, my_test_type='UnitTest'):
     """
     Check if the software profile is correctly generated
-    """
+    """  
     test_node = self.getTestNode()
+    test_node.test_suite_portal = TaskDistributor
+    test_node.test_suite_portal.getTestNode = TaskDistributor.getTestType
     node_test_suite = test_node.getNodeTestSuite('foo')
     self.updateNodeTestSuiteData(node_test_suite, add_third_repository=True)
-    test_node.constructProfile(node_test_suite)
+    node_test_suite.revision = 'rep1=1234-azerty,rep2=3456-qwerty'
+    test_node.constructProfile(node_test_suite,my_test_type)
     self.assertEquals("%s/software.cfg" % (node_test_suite.working_directory,),
                       node_test_suite.custom_profile_path)
     profile = open(node_test_suite.custom_profile_path, 'r')
-    expected_profile = """
+    if my_test_type=='UnitTest':
+      expected_profile = """
 [buildout]
 extends = %(temp_dir)s/testnode/foo/rep0/software.cfg
 
@@ -207,10 +236,27 @@ branch = master
 repository = %(temp_dir)s/testnode/foo/rep2
 branch = foo
 """ % {'temp_dir': self._temp_dir}
+    else:
+      revision1 = "azerty"
+      revision2 = "qwerty"
+      expected_profile = """
+[buildout]
+extends = %(temp_dir)s/testnode/foo/rep0/software.cfg
+
+[rep1]
+repository = <obfuscated_url>/rep1/rep1.git
+revision = %(revision1)s
+ignore-ssl-certificate = true
+
+[rep2]
+repository = <obfuscated_url>/rep2/rep2.git
+revision = %(revision2)s
+ignore-ssl-certificate = true
+""" % {'temp_dir': self._temp_dir, 'revision1': revision1, 'revision2': revision2}
     self.assertEquals(expected_profile, profile.read())
     profile.close()
 
-  def test_05_getAndUpdateFullRevisionList(self):
+  def test_05_getAndUpdateFullRevisionList(self, my_test_type='UnitTest'):
     """
     Check if we clone correctly repositories and get right revisions
     """
@@ -234,7 +280,7 @@ branch = foo
     for vcs_repository in node_test_suite.vcs_repository_list:
       self.assertTrue(os.path.exists(vcs_repository['repository_path']))
 
-  def test_05b_changeRepositoryBranch(self):
+  def test_05b_changeRepositoryBranch(self, my_test_type='UnitTest'):
     """
     It could happen that the branch is changed for a repository. Testnode must
     be able to reset correctly the branch
@@ -307,7 +353,7 @@ branch = foo
     finally:
       Updater.deleteRepository = original_deleteRepository
 
-  def test_06_checkRevision(self):
+  def test_06_checkRevision(self, my_test_type='UnitTest'):
     """
     Check if we are able to restore older commit hash if master decide so
     """
@@ -344,7 +390,7 @@ branch = foo
     self.assertEquals([commit_dict['rep0'][0][0],commit_dict['rep1'][1][0]],
                       getRepInfo(hash=1))
 
-  def test_07_checkExistingTestSuite(self):
+  def test_07_checkExistingTestSuite(self, my_test_type='UnitTest'):
     test_node = self.getTestNode()
     test_suite_data = self.getTestSuiteData(add_third_repository=True)
     self.assertEquals([], os.listdir(self.working_directory))
@@ -360,7 +406,7 @@ branch = foo
     test_node.checkOldTestSuite(test_suite_data)
     self.assertEquals(['foo'], os.listdir(self.working_directory))
 
-  def test_08_getSupportedParamaterSet(self):
+  def test_08_getSupportedParamaterSet(self, my_test_type='UnitTest'):
     original_spawn = ProcessManager.spawn
     try:
       def get_help(self, *args, **kw):
@@ -377,7 +423,7 @@ branch = foo
     finally:
       ProcessManager.spawn = original_spawn
 
-  def test_09_runTestSuite(self):
+  def test_09_runTestSuite(self, my_test_type='UnitTest'):
     """
     Check parameters passed to runTestSuite
     Also make sure that --firefox_bin and --xvfb_bin are passed when needed
@@ -385,36 +431,46 @@ branch = foo
     original_getSupportedParameter = ProcessManager.getSupportedParameterSet
     original_spawn = ProcessManager.spawn
     try:
+      # Create a file
       def _createPath(path_to_create, end_path):
         os.makedirs(path_to_create)
         return os.close(os.open(os.path.join(path_to_create,
                                  end_path),os.O_CREAT))
+      
       def get_parameters(self, *args, **kw):
         call_parameter_list.append({'args': [x for x in args], 'kw':kw})
+      
       def patch_getSupportedParameterSet(self, run_test_suite_path, parameter_list,):
        if '--firefox_bin' and '--xvfb_bin' in parameter_list:
          return set(['--firefox_bin','--xvfb_bin'])
        else:
          return []
+      
       test_node = self.getTestNode()
-      test_node.slapos_controler = SlapOSControler(self.working_directory,
-                                               test_node.config, self.log)
+      RunnerClass = self.returnGoodClassRunner(my_test_type)
+      runner = RunnerClass(test_node)
+      # Create and initialise/regenerate a nodetestsuite
       node_test_suite = test_node.getNodeTestSuite('foo')
       self.updateNodeTestSuiteData(node_test_suite)
       node_test_suite.revision = 'dummy'
+      # Path to the dummy runable
       run_test_suite_path = _createPath(
-          os.path.join(test_node.slapos_controler.instance_root,'a/bin'),'runTestSuite')
+          os.path.join(runner.slapos_controler.instance_root,'a/bin'),'runTestSuite')
+
       def checkRunTestSuiteParameters(additional_parameter_list=None):
         ProcessManager.getSupportedParameterSet = patch_getSupportedParameterSet
         ProcessManager.spawn = get_parameters
-        test_node.runTestSuite(node_test_suite,"http://foo.bar")
+        RunnerClass = self.returnGoodClassRunner(my_test_type)
+        runner = RunnerClass(test_node)
+        runner.runTestSuite(node_test_suite,"http://foo.bar")
         expected_parameter_list = ['%s/a/bin/runTestSuite'
-             %(test_node.slapos_controler.instance_root), '--test_suite', 'Foo', '--revision',
-             'dummy', '--test_suite_title', 'Foo-Test', '--node_quantity', 3, '--master_url',
-             'http://foo.bar']
+           %(runner.slapos_controler.instance_root), '--test_suite', 'Foo', '--revision',
+           'dummy', '--test_suite_title', 'Foo-Test', '--node_quantity', 3, '--master_url',
+           'http://foo.bar']
         if additional_parameter_list:
           expected_parameter_list.extend(additional_parameter_list)
         self.assertEqual(call_parameter_list[0]['args'], expected_parameter_list)
+
       call_parameter_list = []
       checkRunTestSuiteParameters()
       _createPath(os.path.join(test_node.config['slapos_directory'], 'soft/a/parts/firefox'),'firefox-slapos')
@@ -427,12 +483,14 @@ branch = foo
         '%s/soft/a/parts/xserver/bin/Xvfb'
           %(test_node.config['slapos_directory'])])
     finally:
-      ProcessManager.getSupportedParameterSet   = original_getSupportedParameter
+      ProcessManager.getSupportedParameterSet = original_getSupportedParameter
       ProcessManager.spawn = original_spawn
 
-  def test_10_prepareSlapOS(self):
+  def test_10_prepareSlapOS(self, my_test_type='UnitTest'):
     test_node = self.getTestNode()
     test_node_slapos = SlapOSInstance()
+    RunnerClass = self.returnGoodClassRunner(my_test_type)
+    runner = RunnerClass(test_node)
     node_test_suite = test_node.getNodeTestSuite('foo')
     node_test_suite.edit(working_directory=self.working_directory)
     status_dict = {"status_code" : 0}
@@ -448,39 +506,72 @@ branch = foo
                          "args": [x for x in args],
                           "kw": kw})
         return {"status_code": self.status_code}
+    
     SlapOSControler.initializeSlapOSControler = Patch("initializeSlapOSControler")
     SlapOSControler.runSoftwareRelease = Patch("runSoftwareRelease")
     SlapOSControler.runComputerPartition = Patch("runComputerPartition")
-    test_node.prepareSlapOSForTestNode(test_node_slapos)
-    self.assertEquals(["initializeSlapOSControler", "runSoftwareRelease"],
+    method_list_for_prepareSlapOSForTestNode = ["initializeSlapOSControler",
+                                                   "runSoftwareRelease"]
+    method_list_for_prepareSlapOSForTestSuite = ["initializeSlapOSControler",
+                                 "runSoftwareRelease", "runComputerPartition"]
+    runner.prepareSlapOSForTestNode(test_node_slapos)
+    self.assertEquals(method_list_for_prepareSlapOSForTestNode,
                       [x["method_name"] for x in call_list])
     call_list = []
-    test_node.prepareSlapOSForTestSuite(node_test_suite)
-    self.assertEquals(["initializeSlapOSControler", "runSoftwareRelease",
-                       "runComputerPartition"],
+    runner.prepareSlapOSForTestSuite(node_test_suite)
+    self.assertEquals(method_list_for_prepareSlapOSForTestSuite,
                       [x["method_name"] for x in call_list])
     call_list = []
     SlapOSControler.runSoftwareRelease = Patch("runSoftwareRelease", status_code=1)
-    self.assertRaises(SubprocessError, test_node.prepareSlapOSForTestSuite,
+    # TODO : write a test for scalability case
+    self.assertRaises(SubprocessError, runner.prepareSlapOSForTestSuite,
                      node_test_suite)
 
-  def test_11_run(self):
+  def test_11_run(self, my_test_type='UnitTest', grade='master'):
     def doNothing(self, *args, **kw):
         pass
+    # Used in case of 'ScalabilityTest'
+    def patch_getTestType(self, *args, **kw):
+      return my_test_type
+    def patch_getSlaposAccountKey(self, *args, **kw):
+      return "key"
+    def patch_getSlaposAccountCertificate(self, *args, **kw):
+      return "Certificate"
+    def patch_getSlaposUrl(self, *args, **kw):
+      return "http://Foo"
+    def patch_getSlaposHateoasUrl(self, *args, **kw):
+      return "http://Foo"
+    def patch_generateConfiguration(self, *args, **kw):
+      return json.dumps({"configuration_list": [], "involved_nodes_computer_guid"\
+: [], "error_message": "No error.", "launcher_nodes_computer_guid": [], \
+"launchable": False, "randomized_path" : "azertyuiop"})
+    def patch_isMasterTestnode(self, *args, **kw):
+      return (grade == 'master')
+    def patch_isHostingSubscriptionReady(self, *args, **kw):
+      return True
+    def patch_isRegisteredHostingSubscription(self, *args, **kw):
+      return True      
     test_self = self
     test_result_path_root = os.path.join(test_self._temp_dir,'test/results')
     os.makedirs(test_result_path_root)
     global counter
     counter = 0
-    def patch_startTestSuite(self,test_node_title):
+    def patch_startTestSuite(self,node_title,computer_guid='unknown'):
       global counter
       config_list = []
+      # Sclalability slave testnode is not directly in charge of testsuites
+      if my_test_type == 'ScalabilityTest' and grade == 'slave':
+        if counter == 5:
+          raise StopIteration
+        counter += 1
+        return json.dumps([])
+          
       def _checkExistingTestSuite(reference_set):
         test_self.assertEquals(set(reference_set),
-                    set(os.listdir(test_node.config["working_directory"])))
+                  set(os.listdir(test_node.working_directory)))
         for x in reference_set:
           test_self.assertTrue(os.path.exists(os.path.join(
-                               test_node.config["working_directory"],x)),True)
+                             test_node.working_directory,x)),True)
       if counter == 0:
         config_list.append(test_self.getTestSuiteData(reference='foo')[0])
         config_list.append(test_self.getTestSuiteData(reference='bar')[0])
@@ -514,28 +605,75 @@ branch = foo
         result =  TestResultProxy(self._proxy, self._retry_time,
                 self._logger, test_result_path, node_title, revision)
       return result
+    def patch_runTestSuite(self, *argv, **kw):
+      return {'status_code':0}
     original_sleep = time.sleep
     time.sleep = doNothing
     self.generateTestRepositoryList()
+    RunnerClass = self.returnGoodClassRunner(my_test_type)
+    # Patch
+    if my_test_type == "ScalabilityTest":
+      original_getSlaposAccountKey = TaskDistributor.getSlaposAccountKey
+      original_getSlaposAccountCertificate = TaskDistributor.getSlaposAccountCertificate
+      original_getSlaposUrl = TaskDistributor.getSlaposUrl
+      original_getSlaposHateoasUrl = TaskDistributor.getSlaposHateoasUrl
+      original_generateConfiguration = TaskDistributor.generateConfiguration
+      original_isMasterTestnode = TaskDistributor.isMasterTestnode
+      original_updateInstanceXML = RunnerClass._updateInstanceXML
+      original_isHostingSubscriptionReady = SlapOSMasterCommunicator.isHostingSubscriptionReady
+      original_isRegisteredHostingSubscription = SlapOSMasterCommunicator.isRegisteredHostingSubscription
+      original_SlapOSMasterCommunicator__init__ = SlapOSMasterCommunicator.__init__
+      TaskDistributor.getSlaposAccountKey = patch_getSlaposAccountKey
+      TaskDistributor.getSlaposAccountCertificate = patch_getSlaposAccountCertificate
+      TaskDistributor.getSlaposUrl = patch_getSlaposUrl
+      TaskDistributor.getSlaposHateoasUrl = patch_getSlaposHateoasUrl
+      TaskDistributor.generateConfiguration = patch_generateConfiguration
+      TaskDistributor.isMasterTestnode = patch_isMasterTestnode
+      RunnerClass._updateInstanceXML = doNothing
+      SlapOSMasterCommunicator.isHostingSubscriptionReady = patch_isHostingSubscriptionReady
+      SlapOSMasterCommunicator.isRegisteredHostingSubscription = patch_isRegisteredHostingSubscription
+      SlapOSMasterCommunicator.__init__ = doNothing
     original_startTestSuite = TaskDistributor.startTestSuite
-    TaskDistributor.startTestSuite = patch_startTestSuite
+    original_subscribeNode = TaskDistributor.subscribeNode
+    original_getTestType = TaskDistributor.getTestType
     original_createTestResult = TaskDistributionTool.createTestResult
+    TaskDistributor.startTestSuite = patch_startTestSuite
+    TaskDistributor.subscribeNode = doNothing
+    TaskDistributor.getTestType = patch_getTestType
     TaskDistributionTool.createTestResult = patch_createTestResult
-    test_node = self.getTestNode()
-    original_prepareSlapOS = test_node._prepareSlapOS
-    test_node._prepareSlapOS = doNothing
-    original_runTestSuite = test_node.runTestSuite
-    test_node.runTestSuite = doNothing
+
+    # TestNode
+    test_node = self.getTestNode()  
+    # Modify class UnitTestRunner(or more after) method 
+    original_prepareSlapOS = RunnerClass._prepareSlapOS
+    original_runTestSuite = RunnerClass.runTestSuite
+    RunnerClass._prepareSlapOS = doNothing
+    RunnerClass.runTestSuite = patch_runTestSuite
     SlapOSControler.initializeSlapOSControler = doNothing
+    # Inside test_node a runner is created using new UnitTestRunner methods
     test_node.run()
     self.assertEquals(5, counter)
     time.sleep = original_sleep
+    # Restore old class methods
+    if my_test_type == "ScalabilityTest":
+      TaskDistributor.getSlaposAccountKey = original_getSlaposAccountKey
+      TaskDistributor.getSlaposAccountCertificate = original_getSlaposAccountCertificate
+      TaskDistributor.getSlaposUrl = original_getSlaposUrl
+      TaskDistributor.getSlaposHateoasUrl = original_getSlaposHateoasUrl
+      TaskDistributor.generateConfiguration = original_generateConfiguration
+      TaskDistributor.isMasterTestnode = original_isMasterTestnode
+      RunnerClass._updateInstanceXML = original_updateInstanceXML
+      SlapOSMasterCommunicator.isHostingSubscriptionReady = original_isHostingSubscriptionReady
+      SlapOSMasterCommunicator.isRegisteredHostingSubscription = original_isRegisteredHostingSubscription
+      SlapOSMasterCommunicator.__init__ = original_SlapOSMasterCommunicator__init__
     TaskDistributor.startTestSuite = original_startTestSuite
     TaskDistributionTool.createTestResult = original_createTestResult
-    test_node._prepareSlapOS = original_prepareSlapOS
-    test_node.runTestSuite = original_runTestSuite
+    TaskDistributionTool.subscribeNode = original_subscribeNode
+    TaskDistributionTool.getTestType = original_getTestType
+    RunnerClass._prepareSlapOS = original_prepareSlapOS
+    RunnerClass.runTestSuite = original_runTestSuite
 
-  def test_12_spawn(self):
+  def test_12_spawn(self, my_test_type='UnitTest'):
     def _checkCorrectStatus(expected_status,*args):
       result = process_manager.spawn(*args)
       self.assertEqual(result['status_code'], expected_status)
@@ -545,7 +683,7 @@ branch = foo
     # it will be automatically killed
     self.assertRaises(SubprocessError, process_manager.spawn, 'sleep','3')
 
-  def test_13_SlaposControlerResetSoftware(self):
+  def test_13_SlaposControlerResetSoftware(self, my_test_type='UnitTest'):
     test_node = self.getTestNode()
     controler = SlapOSControler(self.working_directory,
                                 test_node.config, self.log)
@@ -557,7 +695,7 @@ branch = foo
     controler._resetSoftware()
     self.assertEquals([], os.listdir(controler.software_root))
 
-  def test_14_createFolder(self):
+  def test_14_createFolder(self, my_test_type='UnitTest'):
     test_node = self.getTestNode()
     node_test_suite = test_node.getNodeTestSuite('foo')
     node_test_suite.edit(working_directory=self.working_directory)
@@ -572,15 +710,37 @@ branch = foo
     createFolder(folder, clean=True)
     self.assertEquals(False, os.path.exists(to_drop_path))
 
-  def test_15_suite_log_directory(self):
+  def test_15_suite_log_directory(self, my_test_type='UnitTest', grade='master'):
     def doNothing(self, *args, **kw):
-        pass
+      pass
+    # Used in case of 'ScalabilityTest'
+    def patch_getTestType(self, *args, **kw):
+      return my_test_type
+    def patch_getSlaposAccountKey(self, *args, **kw):
+      return "key"
+    def patch_getSlaposAccountCertificate(self, *args, **kw):
+      return "Certificate"
+    def patch_getSlaposUrl(self, *args, **kw):
+      return "http://Foo"
+      return "Certificate"
+    def patch_getSlaposHateoasUrl(self, *args, **kw):
+      return "http://Foo"
+    def patch_generateConfiguration(self, *args, **kw):
+      return json.dumps({"configuration_list": [], "involved_nodes_computer_guid"\
+: [], "error_message": "No error.", "launcher_nodes_computer_guid": [], \
+"launchable": False, "randomized_path" : "azertyuiop"})
+    def patch_isMasterTestnode(self, *args, **kw):
+      return grade == 'master'
+    def patch_isHostingSubscriptionReady(self, *args, **kw):
+      return True
+    def patch_isRegisteredHostingSubscription(self, *args, **kw):
+      return True     
     test_self = self
     test_result_path_root = os.path.join(test_self._temp_dir,'test/results')
     os.makedirs(test_result_path_root)
     global counter
     counter = 0
-    def patch_startTestSuite(self,test_node_title):
+    def patch_startTestSuite(self,node_title,computer_guid='unknown'):
       global counter
       config_list = [test_self.getTestSuiteData(reference='aa')[0],
                      test_self.getTestSuiteData(reference='bb')[0]]
@@ -596,6 +756,8 @@ branch = foo
       result = TestResultProxy(self._proxy, self._retry_time,
                self._logger, test_result_path, node_title, revision)
       return result
+    def patch_runTestSuite(self,*argv, **kw):
+      return {'status_code':0}
     def checkTestSuite(test_node):
       test_node.node_test_suite_dict
       rand_part_set = set()
@@ -616,29 +778,82 @@ branch = foo
         self.assertEquals(1, len([x for x in suite_log.readlines() \
                               if x.find("Activated logfile")>=0]))
 
+    RunnerClass = self.returnGoodClassRunner(my_test_type)
     original_sleep = time.sleep
     time.sleep = doNothing
     self.generateTestRepositoryList()
+    if my_test_type == "ScalabilityTest":
+      original_getSlaposAccountKey = TaskDistributor.getSlaposAccountKey
+      original_getSlaposAccountCertificate = TaskDistributor.getSlaposAccountCertificate
+      original_getSlaposUrl = TaskDistributor.getSlaposUrl
+      original_getSlaposHateoasUrl = TaskDistributor.getSlaposHateoasUrl
+      original_generateConfiguration = TaskDistributor.generateConfiguration
+      original_isMasterTestnode = TaskDistributor.isMasterTestnode
+      original_supply = SlapOSControler.supply
+      original_request = SlapOSControler.request
+      original_updateInstanceXML = RunnerClass._updateInstanceXML
+      original_isHostingSubscriptionReady = SlapOSMasterCommunicator.isHostingSubscriptionReady
+      original_isRegisteredHostingSubscription = SlapOSMasterCommunicator.isRegisteredHostingSubscription
+      original_SlapOSMasterCommunicator__init__ = SlapOSMasterCommunicator.__init__
+      TaskDistributor.getSlaposAccountKey = patch_getSlaposAccountKey
+      TaskDistributor.getSlaposAccountCertificate = patch_getSlaposAccountCertificate
+      TaskDistributor.getSlaposUrl = patch_getSlaposUrl
+      TaskDistributor.getSlaposHateoasUrl = patch_getSlaposHateoasUrl
+      TaskDistributor.generateConfiguration = patch_generateConfiguration
+      TaskDistributor.isMasterTestnode = patch_isMasterTestnode
+      SlapOSControler.supply = doNothing
+      SlapOSControler.request = doNothing
+      RunnerClass._updateInstanceXML = doNothing
+      SlapOSMasterCommunicator.isHostingSubscriptionReady = patch_isHostingSubscriptionReady
+      SlapOSMasterCommunicator.isRegisteredHostingSubscription = patch_isRegisteredHostingSubscription
+      SlapOSMasterCommunicator.__init__ = doNothing
     original_startTestSuite = TaskDistributor.startTestSuite
+    original_subscribeNode = TaskDistributor.subscribeNode
+    original_getTestType = TaskDistributor.getTestType
     TaskDistributor.startTestSuite = patch_startTestSuite
+    TaskDistributor.subscribeNode = doNothing
+    TaskDistributor.getTestType = patch_getTestType
     original_createTestResult = TaskDistributionTool.createTestResult
     TaskDistributionTool.createTestResult = patch_createTestResult
     test_node = self.getTestNode()
-    original_prepareSlapOS = test_node._prepareSlapOS
-    test_node._prepareSlapOS = doNothing
-    original_runTestSuite = test_node.runTestSuite
-    test_node.runTestSuite = doNothing
+    # Change UnitTestRunner class methods
+    original_prepareSlapOS = RunnerClass._prepareSlapOS
+
+    original_runTestSuite = RunnerClass.runTestSuite
+ 
+    if my_test_type == "ScalabilityTest":
+      RunnerClass.runTestSuite = patch_runTestSuite
+    else:
+      RunnerClass.runTestSuite = doNothing
+
+    RunnerClass._prepareSlapOS = doNothing
     SlapOSControler.initializeSlapOSControler = doNothing
     test_node.run()
     self.assertEquals(counter, 3)
     checkTestSuite(test_node)
     time.sleep = original_sleep
+    # Restore old class methods
+    if my_test_type == "ScalabilityTest":
+      TaskDistributor.getSlaposAccountKey = original_getSlaposAccountKey
+      TaskDistributor.getSlaposAccountCertificate = original_getSlaposAccountCertificate
+      TaskDistributor.getSlaposUrl = original_getSlaposUrl
+      TaskDistributor.getSlaposHateoasUrl = original_getSlaposHateoasUrl
+      TaskDistributor.generateConfiguration = original_generateConfiguration
+      TaskDistributor.isMasterTestnode = original_isMasterTestnode
+      SlapOSControler.supply =original_supply
+      SlapOSControler.request = original_request
+      SlapOSControler.updateInstanceXML = original_updateInstanceXML
+      SlapOSMasterCommunicator.isHostingSubscriptionReady = original_isHostingSubscriptionReady
+      SlapOSMasterCommunicator.isRegisteredHostingSubscription = original_isRegisteredHostingSubscription
+      SlapOSMasterCommunicator.__init__ = original_SlapOSMasterCommunicator__init__
     TaskDistributor.startTestSuite = original_startTestSuite
     TaskDistributionTool.createTestResult = original_createTestResult
-    test_node._prepareSlapOS = original_prepareSlapOS
-    test_node.runTestSuite = original_runTestSuite
+    TaskDistributionTool.subscribeNode = original_subscribeNode
+    TaskDistributionTool.getTestType = original_getTestType
+    RunnerClass._prepareSlapOS = original_prepareSlapOS
+    RunnerClass.runTestSuite = original_runTestSuite
 
-  def test_16_cleanupLogDirectory(self):
+  def test_16_cleanupLogDirectory(self, my_test_type='UnitTest'):
     # Make sure that we are able to cleanup old log folders
     test_node = self.getTestNode()
     def check(file_list):
@@ -659,7 +874,7 @@ branch = foo
     test_node._cleanupLog()
     check(set(['a_file']))
 
-  def test_17_cleanupTempDirectory(self):
+  def test_17_cleanupTempDirectory(self, my_test_type='UnitTest'):
     # Make sure that we are able to cleanup old temp folders
     test_node = self.getTestNode()
     temp_directory = self.system_temp_folder
@@ -681,7 +896,7 @@ branch = foo
     test_node._cleanupTemporaryFiles()
     check(set(['something']))
 
-  def test_18_resetSoftwareAfterManyBuildFailures(self):
+  def test_18_resetSoftwareAfterManyBuildFailures(self, my_test_type='UnitTest'):
     """
     Check that after several building failures that the software is resetted
     """
@@ -689,6 +904,8 @@ branch = foo
       SlapOSControler.initializeSlapOSControler
     initial_runSoftwareRelease = SlapOSControler.runSoftwareRelease
     test_node = self.getTestNode()
+    RunnerClass = self.returnGoodClassRunner(my_test_type)
+    runner = RunnerClass(test_node)    
     node_test_suite = test_node.getNodeTestSuite('foo')
     init_call_kw_list = []
     def initializeSlapOSControler(self, **kw):
@@ -698,10 +915,11 @@ branch = foo
     SlapOSControler.initializeSlapOSControler = initializeSlapOSControler
     SlapOSControler.runSoftwareRelease = runSoftwareRelease
     def callPrepareSlapOS():
-      test_node._prepareSlapOS(self.working_directory, node_test_suite,
+      runner._prepareSlapOS(self.working_directory, node_test_suite,
          test_node.log, create_partition=0)
     def callRaisingPrepareSlapos():
       self.assertRaises(SubprocessError, callPrepareSlapOS)
+  
     self.assertEquals(node_test_suite.retry_software_count, 0)
     for x in xrange(0,11):
       callRaisingPrepareSlapos()
@@ -717,3 +935,169 @@ branch = foo
     SlapOSControler.initializeSlapOSControler = \
       initial_initializeSlapOSControler
     SlapOSControler.runSoftwareRelease = initial_runSoftwareRelease
+
+  def test_scalability_01_getDelNodeTestSuite(self, my_test_type='ScalabilityTest'):
+    self.test_01_getDelNodeTestSuite(my_test_type)
+  def test_scalability_02_NodeTestSuiteWorkingDirectory(self, my_test_type='ScalabilityTest'):
+    self.test_02_NodeTestSuiteWorkingDirectory(my_test_type)
+  def test_scalability_03_NodeTestSuiteCheckDataAfterEdit(self, my_test_type='ScalabilityTest'):
+    self.test_03_NodeTestSuiteCheckDataAfterEdit(my_test_type)
+  def test_scalability_04_constructProfile(self, my_test_type='ScalabilityTest'):
+    self.test_04_constructProfile(my_test_type)
+  def test_scalability_05_getAndUpdateFullRevisionList(self, my_test_type='ScalabilityTest'):
+    self.test_05_getAndUpdateFullRevisionList(my_test_type)
+  def test_scalability_05b_changeRepositoryBranch(self, my_test_type='ScalabilityTest'):
+    self.test_05b_changeRepositoryBranch(my_test_type)
+  def test_scalability_06_checkRevision(self, my_test_type='ScalabilityTest'):
+    self.test_06_checkRevision(my_test_type)
+  def test_scalability_07_checkExistingTestSuite(self, my_test_type='ScalabilityTest'):
+    self.test_07_checkExistingTestSuite(my_test_type)
+  def test_scalability_08_getSupportedParamaterSet(self, my_test_type='ScalabilityTest'):
+    self.test_08_getSupportedParamaterSet(my_test_type)
+  def test_scalability_09_runTestSuite(self, my_test_type='ScalabilityTest'):
+    # TODO : write own scalability test
+    pass
+  def test_scalability_10_prepareSlapOS(self, my_test_type='ScalabilityTest'):
+    # TODO : write own scalability test
+    # This case test may be dispensable on ScalabilityTest case
+    # so..
+    pass
+  def test_scalability_as_master_11_run(self, my_test_type='ScalabilityTest'):
+    self.test_11_run(my_test_type, grade='master')
+  # TODO : add a test with master and a launchable testsuite -> patch a lot of methods
+  def test_scalability_as_slave_11_run(self, my_test_type='ScalabilityTest'):
+    self.test_11_run(my_test_type, grade='slave')
+  def test_scalability_12_spawn(self, my_test_type='ScalabilityTest'):
+    self.test_12_spawn(my_test_type)
+  def test_scalability_13_SlaposControlerResetSoftware(self, my_test_type='ScalabilityTest'):
+    self.test_13_SlaposControlerResetSoftware(my_test_type)
+  def test_scalability_14_createFolder(self, my_test_type='ScalabilityTest'):
+    self.test_14_createFolder(my_test_type)
+  def test_scalability_as_master_15_suite_log_directory(self, my_test_type='ScalabilityTest'):
+    self.test_15_suite_log_directory(my_test_type, grade='master')
+  def test_scalability_as_slave_15_suite_log_directory(self, my_test_type='ScalabilityTest'):
+    self.test_15_suite_log_directory(my_test_type, grade='slave')
+  def test_scalability_16_cleanupLogDirectory(self, my_test_type='ScalabilityTest'):
+    self.test_16_cleanupLogDirectory(my_test_type)
+  def test_scalability_17_cleanupTempDirectory(self, my_test_type='ScalabilityTest'):
+    self.test_17_cleanupTempDirectory(my_test_type)
+  def test_scalability_18_resetSoftwareAfterManyBuildFailures(self, my_test_type='ScalabilityTest'):
+    # TODO : write own scalability test
+    pass
+
+  def test_zzzz_scalability_19_xxxx(self):
+    # TODO : fill the dummy slapos answer
+    # by patching isSoftwareReleaseReady method.
+    def patch_createTestResult(self, revision, test_name_list, node_title,
+            allow_restart=False, test_title=None, project_title=None):
+      test_result_path = os.path.join(test_result_path_root, test_title)
+      result =  TestResultProxy(self._proxy, self._retry_time,
+                self._logger, test_result_path, node_title, revision)
+      return result
+    global startTestSuiteDone
+    startTestSuiteDone = False
+    def patch_startTestSuite(self,node_title,computer_guid='unknown'):
+      config_list = []
+      global startTestSuiteDone
+      if not startTestSuiteDone:
+        startTestSuiteDone = True
+        config_list.append(test_self.getTestSuiteData(reference='foo')[0])
+        config_list.append(test_self.getTestSuiteData(reference='bar')[0])
+      else:
+        raise StopIteration
+      return json.dumps(config_list)
+    def patch_isMasterTestnode(self, *args, **kw):
+      return True
+    def patch_generateConfiguration(self, *args, **kw):
+      return json.dumps({"configuration_list": [{"ok":"ok"}], "involved_nodes_computer_guid"\
+: ["COMP1", "COMP2", "COMP3"], "error_message": "No error.", "launcher_nodes_computer_guid": ["COMP1"], \
+"launchable": True, "randomized_path" : "azertyuiop"})
+    def doNothing(self, *args, **kw):
+        pass
+    def patch_getSlaposAccountKey(self, *args, **kw):
+      return "key"
+    def patch_getSlaposAccountCertificate(self, *args, **kw):
+      return "Certificate"
+    def patch_getSlaposUrl(self, *args, **kw):
+      return "http://Foo"
+      return "Certificate"
+    def patch_getSlaposHateoasUrl(self, *args, **kw):
+      return "http://Foo"
+    def patch_getTestType(self, *args, **kw):
+      return "ScalabilityTest"
+    def patch_isHostingSubscriptionReady(self, *args, **kw):
+      return True
+    def patch_isRegisteredHostingSubscription(self, *args, **kw):
+      return True     
+    def patch_runTestSuite(self, *args, **kw):
+      return {'status_code':0}
+    test_self = self
+    test_result_path_root = os.path.join(test_self._temp_dir,'test/results')
+    os.makedirs(test_result_path_root)
+    self.generateTestRepositoryList()
+    # Select the good runner to modify
+    RunnerClass = self.returnGoodClassRunner('ScalabilityTest')
+    # Patch methods
+    original_sleep = time.sleep
+    original_getSlaposAccountKey = TaskDistributor.getSlaposAccountKey
+    original_getSlaposAccountCertificate = TaskDistributor.getSlaposAccountCertificate
+    original_getSlaposUrl = TaskDistributor.getSlaposUrl
+    original_getSlaposHateoasUrl = TaskDistributor.getSlaposHateoasUrl
+    original_generateConfiguration = TaskDistributor.generateConfiguration
+    original_isMasterTestnode = TaskDistributor.isMasterTestnode
+    original_startTestSuite = TaskDistributor.startTestSuite
+    original_subscribeNode = TaskDistributor.subscribeNode
+    original_getTestType = TaskDistributor.getTestType
+    original_createTestResult = TaskDistributionTool.createTestResult
+    original_prepareSlapOS = RunnerClass._prepareSlapOS
+    original_runTestSuite = RunnerClass.runTestSuite
+    original_supply = SlapOSControler.supply
+    original_request = SlapOSControler.request
+    original_updateInstanceXML = SlapOSControler.updateInstanceXML
+    original_isHostingSubscriptionReady = SlapOSMasterCommunicator.isHostingSubscriptionReady
+    original_isRegisteredHostingSubscription = SlapOSMasterCommunicator.isRegisteredHostingSubscription
+    original_SlapOSMasterCommunicator__init__ = SlapOSMasterCommunicator.__init__
+
+    #
+    time.sleep = doNothing
+    TaskDistributor.getSlaposAccountKey = patch_getSlaposAccountKey
+    TaskDistributor.getSlaposAccountCertificate = patch_getSlaposAccountCertificate
+    TaskDistributor.getSlaposUrl = patch_getSlaposUrl
+    TaskDistributor.getSlaposHateoasUrl = patch_getSlaposHateoasUrl
+    TaskDistributor.generateConfiguration = patch_generateConfiguration
+    TaskDistributor.isMasterTestnode = patch_isMasterTestnode
+    TaskDistributor.startTestSuite = patch_startTestSuite
+    TaskDistributor.subscribeNode = doNothing
+    TaskDistributor.getTestType = patch_getTestType    
+    TaskDistributionTool.createTestResult = patch_createTestResult
+    RunnerClass._prepareSlapOS = doNothing
+    RunnerClass.runTestSuite = patch_runTestSuite
+    SlapOSControler.supply = doNothing
+    SlapOSControler.request = doNothing
+    SlapOSControler.updateInstanceXML = doNothing
+    SlapOSMasterCommunicator.isHostingSubscriptionReady = patch_isHostingSubscriptionReady
+    SlapOSMasterCommunicator.isRegisteredHostingSubscription = patch_isRegisteredHostingSubscription
+    SlapOSMasterCommunicator.__init__ = doNothing
+    # Run
+    test_node = self.getTestNode()  
+    test_node.run()
+    # Restore methods
+    TaskDistributor.getSlaposAccountKey = original_getSlaposAccountKey
+    TaskDistributor.getSlaposAccountCertificate = original_getSlaposAccountCertificate
+    TaskDistributor.getSlaposUrl = original_getSlaposUrl
+    TaskDistributor.getSlaposHateoasUrl = original_getSlaposHateoasUrl
+    TaskDistributor.generateConfiguration = original_generateConfiguration
+    TaskDistributor.isMasterTestnode = original_isMasterTestnode
+    TaskDistributor.startTestSuite = original_startTestSuite
+    TaskDistributionTool.createTestResult = original_createTestResult
+    TaskDistributionTool.subscribeNode = original_subscribeNode
+    TaskDistributionTool.getTestType = original_getTestType
+    RunnerClass._prepareSlapOS = original_prepareSlapOS
+    RunnerClass.runTestSuite = original_runTestSuite
+    SlapOSControler.supply = original_supply
+    SlapOSControler.request = original_request
+    SlapOSControler.updateInstanceXML = original_updateInstanceXML
+    SlapOSMasterCommunicator.isHostingSubscriptionReady = original_isHostingSubscriptionReady
+    SlapOSMasterCommunicator.isRegisteredHostingSubscription = original_isRegisteredHostingSubscription
+    SlapOSMasterCommunicator.__init__ = original_SlapOSMasterCommunicator__init__
+    time.sleep =original_sleep
diff --git a/erp5/util/taskdistribution/__init__.py b/erp5/util/taskdistribution/__init__.py
index ba39cedbae37aea99325771db74590a748dbd38f..b60adcb4538520f168295a93e8910c70345efdbe 100644
--- a/erp5/util/taskdistribution/__init__.py
+++ b/erp5/util/taskdistribution/__init__.py
@@ -138,6 +138,15 @@ class TestResultLineProxy(RPCRetry):
     def name(self):
         return self._name
 
+    def isTestCaseAlive(self):
+        """
+        Tell if test result line is still alive on site.
+        """
+        try:
+          return bool(self._retryRPC('isTestCaseAlive', [self._test_result_line_path]))
+        except:
+          raise ValueError('isTestCaseAlive Failed.')
+
     def stop(self, test_count=None, error_count=None, failure_count=None,
             skip_count=None, duration=None, date=None, command=None,
             stdout=None, stderr=None, html_test_result=None, **kw):
@@ -201,6 +210,10 @@ class TestResultProxy(RPCRetry):
         return '<%s(%r, %r, %r) at %x>' % (self.__class__.__name__,
             self._test_result_path, self._node_title, self._revision, id(self))
 
+    @property
+    def test_result_path(self):
+        return self._test_result_path
+
     @property
     def revision(self):
         return self._revision
@@ -350,6 +363,35 @@ class TestResultProxy(RPCRetry):
         if self._watcher_thread is not None:
           self._watcher_thread.join()
 
+    def stop(self):
+        """
+        
+        """
+        return self._retryRPC('stopTest', [self._test_result_path])
+        
+class TestResultProxyProxy(TestResultProxy):
+    """
+    A wrapper/proxy to TestResultProxy
+    """
+    def __init__(self, test_suite_master_url, retry_time, logger, test_result_path,
+                node_title, revision):
+      try:
+        proxy = ServerProxy(
+                test_suite_master_url,
+                allow_none=True,
+            ).portal_task_distribution
+      except:
+        raise ValueError("Cannot instanciate ServerProxy")
+      TestResultProxy.__init__(self, proxy, retry_time, logger, test_result_path,
+                node_title, revision)
+
+    def getRunningTestCase(self):
+      """
+      A proxy to getNextTestCase
+      Return the relative path of the test with the running state
+      """
+      return self._retryRPC('getRunningTestCase', [self._test_result_path])
+
 class ServerProxy(xmlrpclib.ServerProxy):
 
     def __init__(self, *args, **kw):
@@ -427,7 +469,6 @@ class TaskDistributionTool(RPCRetry):
 class TaskDistributor(RPCRetry):
 
     def __init__(self,portal_url,retry_time=64,logger=None):
-
         if logger is None:
            logger = null_logger
         if portal_url is None:
@@ -440,14 +481,67 @@ class TaskDistributor(RPCRetry):
             raise ValueError('Unsupported protocol revision: %r',
                 protocol_revision)
 
-    def startTestSuite(self,node_title):
+    def startTestSuite(self,node_title,computer_guid='unknown'):
       """
         Returns None if no test suite is needed.
         therwise, returns a JSON with all the test suite parameters.
       """
-      result = self._retryRPC('startTestSuite',(node_title,))
+      result = self._retryRPC('startTestSuite',(node_title,computer_guid,))
       return result
 
+    def getTestType(self):
+      """
+        Return the Test Type
+      """
+      result = self._retryRPC('getTestType')
+      return result
+
+    def subscribeNode(self, node_title, computer_guid):
+      """
+        Susbscribes node with the node title and the computer guid.
+      """
+      self._retryRPC('subscribeNode', (node_title,computer_guid,))
+
+
+    def generateConfiguration(self, test_suite_title):
+      """
+        Generates a configuration from a test_suite_title
+      """
+      return self._retryRPC('generateConfiguration', (test_suite_title,))
+
+
+    def isMasterTestnode(self, test_node_title):
+      """
+        Returns True or False if the testnode is the master
+      """
+      return self._retryRPC('isMasterTestnode', (test_node_title,))
+
+    def getSlaposAccountKey(self):
+      """
+        Returns the slapos account key related to the distributor
+      """
+      return self._retryRPC('getSlaposAccountKey')
+    
+    def getSlaposAccountCertificate(self):
+      """
+        Returns the slapos account certificate related to the distributor
+      """
+      return self._retryRPC('getSlaposAccountCertificate')
+
+    def getSlaposUrl(self):
+      """
+        Returns the url of slapos master related to the distributor
+      """
+      return self._retryRPC('getSlaposUrl')
+      
+    def getSlaposHateoasUrl(self):
+      """
+        Returns the url of API REST using hateoas of
+        slapos master related to the distributor
+      """
+      return self._retryRPC('getSlaposHateoasUrl')
+
+
 class DummyTaskDistributionTool(object):
     """
     Fake remote server.
diff --git a/erp5/util/testnode/NodeTestSuite.py b/erp5/util/testnode/NodeTestSuite.py
new file mode 100644
index 0000000000000000000000000000000000000000..523aa589db1521e3c7b5179a5b388f3b8f73fa06
--- /dev/null
+++ b/erp5/util/testnode/NodeTestSuite.py
@@ -0,0 +1,106 @@
+##############################################################################
+#
+# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved.
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly advised to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+from datetime import datetime,timedelta
+import os
+import subprocess
+import sys
+import time
+import glob
+import SlapOSControler
+import json
+import time
+import shutil
+import logging
+import string
+import random
+from ProcessManager import SubprocessError, ProcessManager, CancellationError
+from subprocess import CalledProcessError
+from Updater import Updater
+from erp5.util import taskdistribution
+
+class SlapOSInstance(object):
+  """
+  Base of an software instance,
+  store variables used during software installation
+  """
+  def __init__(self):
+    self.retry_software_count = 0
+    self.retry = False
+
+  def edit(self, **kw):
+    self.__dict__.update(**kw)
+    self._checkData()
+
+  def _checkData(self):
+    pass
+    
+    
+class NodeTestSuite(SlapOSInstance):
+  """
+  
+  """
+  def __init__(self, reference):
+    super(NodeTestSuite, self).__init__()
+    self.reference = reference
+
+  def edit(self, **kw):
+    super(NodeTestSuite, self).edit(**kw)
+
+  def _checkData(self):
+    if getattr(self, "working_directory", None) is not None:
+      if not(self.working_directory.endswith(os.path.sep + self.reference)):
+        self.working_directory = os.path.join(self.working_directory,
+                                             self.reference)
+      SlapOSControler.createFolder(self.working_directory)
+      self.test_suite_directory = os.path.join(
+                                   self.working_directory, "test_suite")
+      self.custom_profile_path = os.path.join(self.working_directory,
+                                 'software.cfg')
+    if getattr(self, "vcs_repository_list", None) is not None:
+      for vcs_repository in self.vcs_repository_list:
+        buildout_section_id = vcs_repository.get('buildout_section_id', None)
+        repository_id = buildout_section_id or \
+                        vcs_repository.get('url').split('/')[-1].split('.')[0]
+        repository_path = os.path.join(self.working_directory,repository_id)
+        vcs_repository['repository_id'] = repository_id
+        vcs_repository['repository_path'] = repository_path
+
+  def createSuiteLog(self):
+    # /srv/slapgrid/slappartXX/srv/var/log/testnode/az-mlksjfmlk234Sljssdflkj23KSdfslj/suite.log
+    alphabets = string.digits + string.letters
+    rand_part = ''.join(random.choice(alphabets) for i in xrange(32))
+    random_suite_folder_id = '%s-%s' % (self.reference, rand_part)
+    suite_log_directory = os.path.join(self.log_directory,
+                                       random_suite_folder_id)
+    SlapOSControler.createFolders(suite_log_directory)
+    self.suite_log_path = os.path.join(suite_log_directory,
+                                       'suite.log')
+    return self.getSuiteLogPath(), random_suite_folder_id
+
+  def getSuiteLogPath(self):
+    return getattr(self,"suite_log_path", None)
+
diff --git a/erp5/util/testnode/ScalabilityTestRunner.py b/erp5/util/testnode/ScalabilityTestRunner.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f2379221d5b78eacf92ca3a92e8f0cd02790d95
--- /dev/null
+++ b/erp5/util/testnode/ScalabilityTestRunner.py
@@ -0,0 +1,513 @@
+##############################################################################
+#
+# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved.
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly advised to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+import datetime
+import os
+import subprocess
+import sys
+import time
+import glob
+import SlapOSControler
+import SlapOSMasterCommunicator
+import json
+import time
+import shutil
+import logging
+import string
+import random
+import Utils
+from ProcessManager import SubprocessError, ProcessManager, CancellationError
+from subprocess import CalledProcessError
+from Updater import Updater
+from erp5.util import taskdistribution
+# for dummy slapos answer
+import signal
+
+# max time to instance changing state: 2 hour
+MAX_INSTANCE_TIME = 60*60*2
+# max time to register instance to slapOSMaster: 5 minutes
+MAX_CREATION_INSTANCE_TIME = 60*10
+# max time for a test: 1 hour
+MAX_TEST_CASE_TIME = 60*60
+
+class ScalabilityTestRunner():
+  def __init__(self, testnode):
+    self.testnode =  testnode
+    self.log = self.testnode.log
+    
+    self.slapos_controler = SlapOSControler.SlapOSControler(
+                                  self.testnode.working_directory,
+                                  self.testnode.config,
+                                  self.log)
+    # Create the slapos account configuration file and dir
+    key = self.testnode.test_suite_portal.getSlaposAccountKey()
+    certificate = self.testnode.test_suite_portal.getSlaposAccountCertificate()
+
+    # Get Slapos Master Url
+    self.slapos_url = ''
+    try:
+      self.slapos_url = self.testnode.test_suite_portal.getSlaposUrl()
+      if not self.slapos_url:
+        self.slapos_url = self.testnode.config['server_url']
+    except:
+      self.slapos_url = self.testnode.config['server_url']
+    
+    # Get Slapos Master url used for api rest (using hateoas)
+    self.slapos_api_rest_url = self.testnode.test_suite_portal.getSlaposHateoasUrl()
+
+    self.log("SlapOS Master url is: %s" %self.slapos_url)
+    self.log("SlapOS Master hateoas url is: %s" %self.slapos_api_rest_url)
+    
+    self.key_path, self.cert_path, config_path = self.slapos_controler.createSlaposConfigurationFileAccount(
+                                        key, certificate, self.slapos_url, self.testnode.config)
+    self.slapos_communicator = None
+    # Dict containing used to store which SR is not yet correctly installed.
+    # looks like: {'comp_id1':'SR_urlA', 'comp_id2':'SR_urlA',..}
+    self.remaining_software_installation_dict = {}
+    
+    # Protection to prevent installation of softwares after checking
+    self.authorize_supply = True
+    self.authorize_request = False
+    # Used to simulate SlapOS answer (used as a queue)
+    self.last_slapos_answer = []
+    self.last_slapos_answer_request = []
+    
+  def _prepareSlapOS(self, software_path, computer_guid, create_partition=0):
+    # create_partition is kept for compatibility
+    """
+    A proxy to supply : Install a software on a specific node
+    """
+    self.log("testnode, supply : %s %s", software_path, computer_guid)
+    if self.authorize_supply :
+      self.remaining_software_installation_dict[computer_guid] = software_path
+      self.slapos_controler.supply(software_path, computer_guid)
+      # Here make a request via slapos controler ?
+      return {'status_code' : 0}                                          
+    else:
+      raise ValueError("Too late to supply now. ('self.authorize_supply' is False)")
+      return {'status_code' : 1}     
+
+  def _generateInstanceTitle(self, test_suite_title):
+    """
+    Generate an instance title using various parameter
+    TODO : add some verification (to don't use unexisting variables)
+    """
+    instance_title = "Scalability-"
+    instance_title += "("+test_suite_title+")-"
+    instance_title += str(self.involved_nodes_computer_guid).replace("'","")
+    instance_title += "-"+str(datetime.datetime.now().isoformat())+"-"
+    instance_title += "timestamp="+str(time.time())
+    return instance_title
+
+  def _generateInstanceXML(self, software_configuration,
+                      test_result, test_suite):
+    """
+    Generate a complete scalability instance XML configuration
+    """
+    config_cluster = software_configuration.copy()
+    config = {'cluster':config_cluster}
+    config.update({'scalability-launcher-computer-guid':self.launcher_nodes_computer_guid[0]})
+    config.update({'scalability-launcher-title':'MyTestNodeTitle'})
+    config.update({'test-result-path':test_result.test_result_path})
+    config.update({'test-suite-revision':test_result.revision})
+    config.update({'test-suite':test_suite})
+    config.update({'test-suite-master-url':self.testnode.config['test_suite_master_url']})
+    return config
+  
+  def _createInstance(self, software_path, software_configuration, instance_title,
+                      test_result, test_suite):
+    """
+    Create scalability instance
+    """
+    if self.authorize_request:
+      config = self._generateInstanceXML(software_configuration,
+                                    test_result, test_suite)
+      self.log("testnode, request : %s", instance_title)
+      config = json.dumps(config)
+      self.slapos_controler.request(instance_title, software_path,
+                             "test", {"_" : config},
+                             self.launcher_nodes_computer_guid[0])
+      self.authorize_request = False
+      return {'status_code' : 0}                                          
+    else:
+      raise ValueError("Softwares release not ready yet to launch instan\
+ces or already launched.")
+      return {'status_code' : 1}  
+      
+  def prepareSlapOSForTestNode(self, test_node_slapos=None):
+    """
+    We will build slapos software needed by the testnode itself,
+    """
+    if self.testnode.test_suite_portal.isMasterTestnode(
+                           self.testnode.config['test_node_title']):
+      pass
+    return {'status_code' : 0} 
+
+  # Dummy slapos answering
+  def _getSignal(self, signal, frame):
+    self.log("Dummy SlapOS Master answer received.")
+    self.last_slapos_answer.append(True)
+  def _prepareDummySlapOSAnswer(self):
+    self.log("Dummy slapOS answer enabled, send signal to %s (kill -10 %s) to simu\
+late a SlapOS (positive) answer." %(str(os.getpid()),str(os.getpid()),))
+    signal.signal(signal.SIGUSR1, self._getSignal)
+  def _comeBackFromDummySlapOS(self):
+    self.log("Dummy slapOS answer disabled, please don't send more signals.")
+    # use SIG_USR (kill)
+    signal.signal(signal.SIGUSR1, signal.SIG_DFL)
+  def simulateSlapOSAnswer(self):
+    if len(self.last_slapos_answer)==0:
+      return False
+    else:
+      return self.last_slapos_answer.pop()
+  # /Dummy slapos answering
+    
+  def isSoftwareReleaseReady(self, software_url, computer_guid):
+    """
+    Return true if the specified software on the specified node is installed.
+    This method should communicates with SlapOS Master.
+    """
+    # TODO : implement -> communication with SlapOS master
+    # this simulate a SlapOS answer
+    return self.simulateSlapOSAnswer()
+  
+  def remainSoftwareToInstall(self):
+    """
+    Return True if it remains softwares to install, otherwise return False
+    """
+    # Remove from grid installed software entries
+    for computer_guid, software_path in self.remaining_software_installation_dict.items():
+      if self.isSoftwareReleaseReady(software_path, computer_guid):
+        del self.remaining_software_installation_dict[computer_guid]
+    # Not empty grid means that all softwares are not installed
+    return len(self.remaining_software_installation_dict) > 0
+
+  def _updateInstanceXML(self, software_configuration, instance_title,
+                      test_result, test_suite):
+    """
+    Just a proxy to SlapOSControler.updateInstanceXML.
+    """
+    config = self._generateInstanceXML(software_configuration,
+                                  test_result, test_suite)
+    config = json.dumps(config)
+    self.log("testnode, updateInstanceXML : %s", instance_title)
+    self.slapos_controler.updateInstanceXML(instance_title, {"_" : config})
+    return {'status_code' : 0} 
+
+  def _waitInstance(self, instance_title, state, max_time=MAX_INSTANCE_TIME):
+    """
+    Wait for 'max_time' an instance specific state
+    """
+    self.log("Wait for instance state: %s" %state)
+    start_time = time.time()
+    while (not self.slapos_communicator.isHostingSubscriptionReady(instance_title, state)
+         and (max_time > (time.time()-start_time))):
+      self.log("Instance(s) not in %s state yet." % state)
+      time.sleep(15)
+    if (time.time()-start_time) > max_time:
+      error_message = "Instance '%s' not '%s' after %s seconds" %(instance_title, state, str(time.time()-start_time))
+      self.log(error_message)
+      self.log("Do you use instance state propagation in your project?")
+      self.log("Instance '%s' will be stopped and test avorted." %instance_title)
+      # What if we wanted to stop ?
+      self.slapos_controler.stopInstance(instance_title)
+      # XXX: _waitInstance call here ? recursive call ?
+      # XXX: sleep 60 seconds.
+      time.sleep(60) 
+      raise ValueError(error_message)
+    self.log("Instance correctly '%s' after %s seconds." %(state, str(time.time()-start_time)))
+
+  def _waitInstanceCreation(self, instance_title, max_time=MAX_CREATION_INSTANCE_TIME):
+    """
+    Wait for 'max_time' the instance creation
+    """
+    self.log("Wait for instance creation")
+    start_time = time.time()
+    while ( not self.slapos_communicator.isRegisteredHostingSubscription(instance_title) \
+         and (max_time > (time.time()-start_time)) ):
+      time.sleep(5)
+    if (time.time()-start_time) > max_time:
+      raise ValueError("Instance '%s' not found after %s seconds" %(instance_title, max_time))
+    self.log("Instance found on slapOSMaster")
+
+  def prepareSlapOSForTestSuite(self, node_test_suite):
+    """
+    Install testsuite softwares
+    """
+    self.log('prepareSlapOSForTestSuite')
+    # Define how many time this method can take
+    max_time = 3600*10*1.0 # 10 hours
+    interval_time = 60
+    start_time = time.time()
+    # Create a communicator with slapos
+    self.log("creating SlapOs Master communicator...")
+    self.slapos_communicator = SlapOSMasterCommunicator.SlapOSMasterCommunicator(
+                                  self.cert_path,
+                                  self.key_path,
+                                  self.log,
+                                  self.slapos_api_rest_url)
+    # Only master testnode must order software installation
+    if self.testnode.test_suite_portal.isMasterTestnode(
+            self.testnode.config['test_node_title']):
+      # Get from ERP5 Master the configuration of the cluster for the test
+      test_configuration = Utils.deunicodeData(
+          json.loads(self.testnode.test_suite_portal.generateConfiguration(
+                      node_test_suite.test_suite_title)
+                    )
+        )
+      self.involved_nodes_computer_guid = test_configuration['involved_nodes_computer_guid']
+      self.launchable = test_configuration['launchable']
+      self.error_message = test_configuration['error_message']
+      self.randomized_path = test_configuration['randomized_path']
+      # Avoid the test if it is not launchable
+      if not self.launchable:
+        self.log("Test suite %s is not actually launchable with \
+  the current cluster configuration." %(node_test_suite.test_suite_title,))
+        self.log("ERP5 Master indicates : %s" %(self.error_message,))
+        # error : wich code to return ?
+        return {'status_code' : 1}
+
+      involved_nodes_computer_guid = test_configuration['involved_nodes_computer_guid']
+      configuration_list = test_configuration['configuration_list']
+      node_test_suite.edit(configuration_list=configuration_list)
+      self.launcher_nodes_computer_guid = test_configuration['launcher_nodes_computer_guid']
+      
+      # Create an obfuscated link to the testsuite directory
+      path_to_suite = os.path.join(
+                      self.testnode.config['working_directory'],
+                      node_test_suite.reference)
+      self.obfuscated_link_path = os.path.join(
+                      self.testnode.config['software_directory'],
+                      self.randomized_path)
+      if ( not os.path.lexists(self.obfuscated_link_path) and
+           not os.path.exists(self.obfuscated_link_path) ) :
+        try :
+          os.symlink(path_to_suite, self.obfuscated_link_path)
+          self.log("testnode, Symbolic link (%s->%s) created."
+                   %(self.obfuscated_link_path, path_to_suite))
+        except :
+          self.log("testnode, Unable to create symbolic link to the testsuite.")
+          raise ValueError("testnode, Unable to create symbolic link to the testsuite.")
+      self.log("Sym link : %s %s" %(path_to_suite, self.obfuscated_link_path))
+      
+      # Construct the ipv6 obfuscated url of the software profile reachable from outside
+      self.reachable_address = os.path.join(
+        "https://","["+self.testnode.config['httpd_ip']+"]"+":"+self.testnode.config['httpd_software_access_port'],
+        self.randomized_path)
+      self.reachable_profile = os.path.join(self.reachable_address, "software.cfg")
+
+      # Write the reachable address in the software.cfg file,
+      # by replacing <obfuscated_url> occurences by the current reachable address.
+      software_file = open(node_test_suite.custom_profile_path, "r")
+      file_content = software_file.readlines()
+      new_file_content = []
+      for line in file_content:
+        new_file_content.append(line.replace('<obfuscated_url>', self.reachable_address))
+      software_file.close()
+      os.remove(node_test_suite.custom_profile_path)
+      software_file = open(node_test_suite.custom_profile_path, "w")
+      for line in new_file_content:
+        software_file.write(line)
+      software_file.close()
+      self.log("Software reachable profile path is : %s "
+                              %(self.reachable_profile,))
+
+      # Ask for SR installation
+      for computer_guid in self.involved_nodes_computer_guid:
+        self._prepareSlapOS(self.reachable_profile, computer_guid) 
+      # From the line below we would not supply any more softwares
+      self.authorize_supply = False
+      # TODO : remove the line below wich simulate an answer from slapos master
+      self._prepareDummySlapOSAnswer()
+      # Waiting until all softwares are installed
+      while ( self.remainSoftwareToInstall() 
+         and (max_time > (time.time()-start_time))):
+        self.log("Master testnode is waiting\
+ for the end of all software installation (for %ss) PID=%s.",
+          str(int(time.time()-start_time)), str(os.getpid()))
+        time.sleep(interval_time)
+      # TODO : remove the line below wich simulate an answer from slapos master
+      self._comeBackFromDummySlapOS()
+      if self.remainSoftwareToInstall() :
+        # All softwares are not installed, however maxtime is elapsed, that's a failure.
+        return {'status_code' : 1}
+      self.authorize_request = True
+      self.log("Softwares installed")
+      # Launch instance
+      self.instance_title = self._generateInstanceTitle(node_test_suite.test_suite_title)
+      try:
+        self._createInstance(self.reachable_profile, configuration_list[0],
+                              self.instance_title, node_test_suite.test_result, node_test_suite.test_suite)
+        self.log("Scalability instance requested.")
+      except:
+        self.log("Unable to launch instance")
+        raise ValueError("Unable to launch instance")
+      self.log("Waiting for instance creation..")
+      self._waitInstanceCreation(self.instance_title)
+      return {'status_code' : 0}
+    return {'status_code' : 1}
+
+  def runTestSuite(self, node_test_suite, portal_url):
+    if not self.launchable:
+      self.log("Current test_suite is not actually launchable.")
+      return {'status_code' : 1} # Unable to continue due to not realizable configuration
+    configuration_list = node_test_suite.configuration_list
+    test_list = range(0, len(configuration_list))
+    # create test_result
+    test_result_proxy = self.testnode.portal.createTestResult(
+      node_test_suite.revision, test_list,
+      self.testnode.config['test_node_title'],
+      True, node_test_suite.test_suite_title,
+      node_test_suite.project_title)
+  
+    count = 0
+    error_message = None
+
+    # Each cluster configuration are tested
+    for configuration in configuration_list:
+
+      # First configuration doesn't need XML configuration update.
+      if count > 0:
+        # Stop instance
+        self.slapos_controler.stopInstance(self.instance_title)
+        self._waitInstance(self.instance_title, 'stopped')
+        # Update instance XML configuration 
+        self._updateInstanceXML(configuration, self.instance_title,
+                      node_test_suite.test_result, node_test_suite.test_suite)
+        self._waitInstance(self.instance_title, 'started')
+        # Start instance
+        self.slapos_controler.startInstance(self.instance_title)
+        
+      # XXX: Dirty hack used to force haproxy to restart in time
+      # with all zope informations.
+      self._waitInstance(self.instance_title, 'started')
+      self.slapos_controler.stopInstance(self.instance_title)
+      self._waitInstance(self.instance_title, 'stopped')
+      self.slapos_controler.startInstance(self.instance_title)
+      ##########################################################
+        
+      self._waitInstance(self.instance_title, 'started')
+      
+      # Start only the current test
+      exclude_list=[x for x in test_list if x!=test_list[count]]
+      count += 1
+      test_result_line_proxy = test_result_proxy.start(exclude_list)
+
+      # 
+      if test_result_line_proxy == None :
+        error_message = "Test case already tested."
+        break
+
+      self.log("Test for count : %d is in a running state." %count)
+
+      # Wait for test case ending
+      test_case_start_time = time.time()
+      while test_result_line_proxy.isTestCaseAlive() and \
+            test_result_proxy.isAlive() and \
+            time.time() - test_case_start_time < MAX_TEST_CASE_TIME:
+        time.sleep(15)
+
+      # Max time limit reach for current test case: failure.
+      if test_result_line_proxy.isTestCaseAlive():
+        error_message = "Test case during for %s seconds, too long. (max: %s seconds). Test failure." \
+                            %(str(time.time() - test_case_start_time), MAX_TEST_CASE_TIME)
+        break
+
+      # Test cancelled, finished or in an undeterminate state.
+      if not test_result_proxy.isAlive():
+        # Test finished
+        if count == len(configuration_list):
+          break
+        # Cancelled or in an undeterminate state.
+        error_message = "Test cancelled or undeterminate state."
+        break
+
+    # Stop current instance
+    self.slapos_controler.stopInstance(self.instance_title)
+    self._waitInstance(self.instance_title, 'stopped')
+
+    # Delete old instances
+    self._cleanUpOldInstance()
+
+    # If error appears then that's a test failure.    
+    if error_message:
+      test_result_line_proxy.stop(error_count=1, failure_count=1,
+                                  stdout=error_message, stderr=error_message)
+      test_result_proxy.reportFailure(stdout=error_message)
+      self.log("Test Failed.")
+      return {'status_code' : 1, 'error_message': error_message} 
+    # Test is finished.
+    self.log("Test finished.")
+    return {'status_code' : 0}
+
+  def _cleanUpOldInstance(self):
+    self.log("_cleanUpOldInstance")
+
+    # Get title and link list of all instances
+    instance_dict = self.slapos_communicator.getHostingSubscriptionDict()
+    instance_to_delete_list = []
+    outdated_date = datetime.datetime.fromtimestamp(time.time()) - datetime.timedelta(days=2)
+
+    # Select instances to delete
+    for title,link in instance_dict.items():
+      # Instances created by testnode contains "Scalability-" and
+      # "timestamp=" in the title.
+      if "Scalability-" in title and "timestamp=" in title:
+        # Get timestamp of the instance creation date
+        foo, timestamp = title.split("timestamp=")
+        creation_date = datetime.datetime.fromtimestamp(float(timestamp))
+        # Test if instance is older than the limit
+        if creation_date < outdated_date:
+          instance_to_delete_list.append((title,link))
+    
+    for title,link in instance_to_delete_list:
+      # Get instance information
+      instance_information_dict = self.slapos_communicator.getHostingSubscriptionInformationDict(title)
+      # Delete instance
+      if instance_information_dict:
+        if instance_information_dict['status'] != 'destroyed':
+          self.slapos_controler.request(
+              instance_information_dict['title'],
+              instance_information_dict['software_url'],
+              software_type=instance_information_dict['software_type'],
+              computer_guid=instance_information_dict['computer_guid'],
+              state='destroyed'
+          )
+          self.log("Instance '%s' deleted." %instance_information_dict['title'])
+
+  def _cleanUpNodesInformation(self):
+    self.involved_nodes_computer_guid = []
+    self.launcher_nodes_computer_guid = []
+    self.remaining_software_installation_dict = {}
+    self.authorize_supply = True
+    self.authorize_request = False
+
+  def getRelativePathUsage(self):
+    """
+    Used by the method testnode.constructProfile() to know
+    if the software.cfg have to use relative path or not.
+    """
+    return True
diff --git a/erp5/util/testnode/SlapOSControler.py b/erp5/util/testnode/SlapOSControler.py
index 27eea04c7d2582b3a5373ca64687acbddf96c736..821613f7c7541cc8612b25db93069064dd74c375 100644
--- a/erp5/util/testnode/SlapOSControler.py
+++ b/erp5/util/testnode/SlapOSControler.py
@@ -33,6 +33,8 @@ import xml_marshaller
 import shutil
 import sys
 import glob
+import argparse
+from slapos import client
 
 MAX_PARTIONS = 10
 MAX_SR_RETRIES = 3
@@ -47,6 +49,20 @@ def createFolders(folder):
   if not(os.path.exists(folder)):
     os.makedirs(folder)
 
+def isDir(folder):
+  return os.path.isdir(folder)
+
+def createFile(path, mode, content):
+  f = open(path, mode)
+  if os.path.exists(path):
+    f.write(content)
+    f.close()
+  else:
+    # error
+    pass
+
+
+
 class SlapOSControler(object):
 
   def __init__(self, working_directory, config, log):
@@ -54,8 +70,175 @@ class SlapOSControler(object):
     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')
     self.log = log
+    self.proxy_database = os.path.join(working_directory, 'proxy.db')
+    self.instance_config = {}
+
+  #TODO: implement a method to get all instance related the slapOS account
+  # and deleting all old instances (based on creation date or name etc...)
+
+  def createSlaposConfigurationFileAccount(self, key, certificate, slapos_url, config):
+    # Create "slapos_account" directory in the "slapos_directory"
+    slapos_account_directory = os.path.join(config['slapos_directory'], "slapos_account")
+    createFolder(slapos_account_directory)
+    # Create slapos-account files
+    slapos_account_key_path = os.path.join(slapos_account_directory, "key")
+    slapos_account_certificate_path = os.path.join(slapos_account_directory, "certificate")
+    configuration_file_path = os.path.join(slapos_account_directory, "slapos.cfg")
+    configuration_file_value = "[slapos]\nmaster_url = %s\n\
+[slapconsole]\ncert_file = %s\nkey_file = %s" %(
+                                  slapos_url,
+                                  slapos_account_certificate_path,
+                                  slapos_account_key_path)
+    createFile(slapos_account_key_path, "w", key)
+    createFile(slapos_account_certificate_path, "w", certificate)
+    createFile(configuration_file_path, "w", configuration_file_value)
+    self.configuration_file_path = configuration_file_path
+    return slapos_account_key_path, slapos_account_certificate_path, configuration_file_path
+
+  def supply(self, software_url, computer_id, state="available"):
+    """
+    Request the installation of a software release on a specific node
+    Ex :
+    my_controler.supply('kvm.cfg', 'COMP-726')
+    """
+    self.log('SlapOSControler : supply')
+    parser = argparse.ArgumentParser()
+    parser.add_argument("configuration_file")
+    parser.add_argument("software_url")
+    parser.add_argument("node")
+    if os.path.exists(self.configuration_file_path):
+      args = parser.parse_args([self.configuration_file_path, software_url, computer_id])
+      config = client.Config()
+      config.setConfig(args, args.configuration_file)
+      try:
+        local = client.init(config)
+        local['supply'](software_url, computer_guid=computer_id, state=state)
+        self.log('SlapOSControler : supply %s %s %s' %(software_url, computer_id, state))
+      except:
+        self.log("SlapOSControler.supply, \
+                 exception in registerOpenOrder", exc_info=sys.exc_info())
+        raise ValueError("Unable to supply (or remove)")
+    else:
+      raise ValueError("Configuration file not found.")
+
+  def destroy(self, software_url, computer_id):
+    """
+    Request Deletetion of a software release on a specific node
+    Ex :
+    my_controler.destroy('kvm.cfg', 'COMP-726')
+    """
+    self.supply(self, software_url, computer_id, state="destroyed")
+    
+  def getInstanceRequestedState(self, reference):
+    try:
+      return self.instance_config[reference]['requested_state']
+    except:
+      raise ValueError("Instance '%s' not exist" %self.instance_config[reference])
+      
+  def request(self, reference, software_url, software_type=None,
+            software_configuration=None, computer_guid=None, state='started'):
+    """
+    configuration_file_path (slapos acount)
+    reference : instance title
+    software_url : software path/url
+    software_type : scalability
+    software_configuration : dict { "_" : "{'toto' : 'titi'}" } 
+
+    Ex :
+    my_controler._request('Instance16h34Ben',
+                               'kvm.cfg', 'cluster', { "_" : "{'toto' : 'titi'}" } )
+
+    """
+    self.log('SlapOSControler : request-->SlapOSMaster')
+    current_intance_config = {'software_type':software_type,
+                              'software_configuration':software_configuration,
+                              'computer_guid':computer_guid,
+                              'software_url':software_url,
+                              'requested_state':state,
+                              'partition':None
+                              }
+    self.instance_config[reference] = current_intance_config
+
+    filter_kw = None
+    if computer_guid != None:
+      filter_kw = { "computer_guid": computer_guid }
+    if os.path.exists(self.configuration_file_path):
+      parser = argparse.ArgumentParser()
+      parser.add_argument("configuration_file")
+      args = parser.parse_args([self.configuration_file_path])
+      config = client.Config()
+      config.setConfig(args, args.configuration_file)
+      try:
+        local = client.init(config)
+        partition = local['request'](
+          software_release = software_url,
+          partition_reference = reference,
+          partition_parameter_kw = software_configuration,
+          software_type = software_type,
+          filter_kw = filter_kw,
+          state = state)
+        self.instance_config[reference]['partition'] = partition
+        if state == 'destroyed':
+          del self.instance_config[reference]
+        if state == 'started':
+          self.log('Instance started with configuration: %s' %str(software_configuration))
+      except:
+        self.log("SlapOSControler.request, \
+              exception in registerOpenOrder", exc_info=sys.exc_info())
+        raise ValueError("Unable to do this request")
+    else:
+      raise ValueError("Configuration file not found.")
+
+  def _requestSpecificState(self, reference, state):
+    self.request(reference,
+        self.instance_config[reference]['software_url'],
+        self.instance_config[reference]['software_type'],
+        self.instance_config[reference]['software_configuration'],
+        self.instance_config[reference]['computer_guid'],
+        state=state
+    )    
+  
+  def destroyInstance(self, reference):
+    self.log('SlapOSControler : delete instance')
+    try:
+      self._requestSpecificState(reference, 'destroyed')
+    except:
+      raise ValueError("Can't delete instance '%s' (instance may not been created?)" %reference)
+    
+  def stopInstance(self, reference):
+    self.log('SlapOSControler : stop instance')
+    try:
+      self._requestSpecificState(reference, 'stopped')
+    except:
+      raise ValueError("Can't stop instance '%s' (instance may not been created?)" %reference)
+  
+  def startInstance(self, reference):
+    self.log('SlapOSControler : start instance')
+    try:
+      self._requestSpecificState(reference, 'started')
+    except:
+      raise ValueError("Can't start instance '%s' (instance may not been created?)" %reference)
+
+  def updateInstanceXML(self, reference, software_configuration):
+    """
+    Update the XML configuration of an instance
+    # Request same instance with different parameters.
+    """
+    self.log('SlapOSControler : updateInstanceXML')
+    self.log('SlapOSControler : updateInstanceXML will request same'
+             'instance with new XML configuration...')
+
+    try:
+      self.request(reference,
+        self.instance_config[reference]['software_url'],
+        self.instance_config[reference]['software_type'],
+        software_configuration,
+        self.instance_config[reference]['computer_guid'],
+        state='started'
+      )
+    except:
+      raise ValueError("Can't update instance '%s' (may not exist?)" %reference)
 
   def _resetSoftware(self):
     self.log('SlapOSControler : GOING TO RESET ALL SOFTWARE : %r' %
@@ -65,7 +248,6 @@ class SlapOSControler(object):
     os.mkdir(self.software_root)
     os.chmod(self.software_root, 0750)
 
-
   def initializeSlapOSControler(self, slapproxy_log=None, process_manager=None,
         reset_software=False, software_path_list=None):
     self.process_manager = process_manager
diff --git a/erp5/util/testnode/SlapOSMasterCommunicator.py b/erp5/util/testnode/SlapOSMasterCommunicator.py
new file mode 100644
index 0000000000000000000000000000000000000000..6eefd2fcdbe764e3bc72d6f245e43dabefa171b7
--- /dev/null
+++ b/erp5/util/testnode/SlapOSMasterCommunicator.py
@@ -0,0 +1,206 @@
+import json
+import httplib
+import urlparse
+import time
+
+TIMEOUT = 30
+
+# TODO: News-> look list to get last news... (and not the first of the list)
+
+class SlapOSMasterCommunicator(object):
+  """
+  Communication with slapos Master using Hateoas.
+  
+  collection: collection of data (hosting_subscription, instance, software_release...)
+  hosting_subscription: result of a request
+  instance(s): instance(s) related to an hosting_subscription
+
+  usage: ex:
+    # Try to reuse same communicator, because initilization step may takes a lot of time
+    # due to listing of all instances (alive or not) related to the specified slapOS account.
+    communicator = SlapOSMasterCommunicator()
+    
+    # Print news related to 'TestScalability_21423104630420' all instances
+    instance_link_list =  communicator._getRelatedInstanceLink('TestScalability_21423104630420')
+    for instance_link in instance_link_list:
+      news = communicator.getNewsFromInstanceLink(instance_link)
+      print news['news']
+  """
+  def __init__(self, certificate_path, key_path, log,
+                     url):
+    # Create connection
+    api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(url)
+    self.log = log
+    self.certificate_path = certificate_path
+    self.key_path = key_path
+    self.url = url
+    self.connection = self._getConnection(self.certificate_path, self.key_path, self.url)
+    # Get master
+    master_link = {'href':api_path,'type':"application/vnd.slapos.org.hal+json; class=slapos.org.master"}
+    master = self._curl(master_link)
+    self.person_link = master['_links']['http://slapos.org/reg/me']
+    # Get person related to specified key/certificate provided
+    person = self._curl(self.person_link)
+    self.personnal_collection_link = person['_links']['http://slapos.org/reg/hosting_subscription']
+    # Get collection (of hosting subscriptions)
+    collection = self._curl(self.personnal_collection_link)
+    # XXX: This part may be extremly long (because here no hosting subscriptions
+    # has been visited)
+    self.hosting_subcriptions_dict = {}
+    self.visited_hosting_subcriptions_link_list = []
+    self.log("SlapOSMasterCommunicator will read all hosting subscriptions entries, "
+             "it may take several time...")
+    self._update_hosting_subscription_informations()
+    
+  def _getConnection(self,certificate_path, key_path, url):
+    api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(url)
+    #self.log("HTTPS Connection with: %s, cert=%s, key=%s" %(api_netloc,key_path,certificate_path))
+    return httplib.HTTPSConnection(api_netloc, key_file=key_path, cert_file=certificate_path, timeout=TIMEOUT)
+
+  def _curl(self, link):
+    """
+    'link' must look like : {'href':url,'type':content_type}
+    """
+    # Set timeout
+    import socket
+    socket.setdefaulttimeout(1.0*TIMEOUT)
+    
+    api_scheme, api_netloc, api_path, api_query, api_fragment = urlparse.urlsplit(link['href'])
+    max_retry = 10
+    # Try to use existing conection
+    try:
+      self.connection.request(method='GET', url=api_path, headers={'Accept': link['type']}, body="")
+      response = self.connection.getresponse()
+      return json.loads(response.read())
+    # Create and use new connection
+    except:
+      retry = 0
+      # (re)Try several time to use new connection
+      while retry < max_retry:
+        try:
+          self.connection = self._getConnection(self.certificate_path, self.key_path, self.url)
+          self.connection.request(method='GET', url=api_path, headers={'Accept': link['type']}, body="")
+          response = self.connection.getresponse()
+          return json.loads(response.read())
+        except:
+          self.log("SlapOSMasterCommunicator: Connection failed..")
+          retry += 1
+          time.sleep(10)
+    self.log("SlapOSMasterCommunicator: All connection attempts failed after %d try.." %max_retry)
+    raise ValueError("SlapOSMasterCommunicator: Impossible to use connection")
+        
+  def _update_hosting_subscription_informations(self):
+    """
+    Add all not already visited hosting_subcription
+    # Visit all hosting subscriptions and fill a dict containing all
+    # new hosting subscriptions. ( like: {hs1_title:hs1_link, hs2_title:hs2_link, ..} )
+    # and a list of visited hosting_subsciption ( like: [hs1_link, hs2_link, ..] )
+    """
+    collection = self._curl(self.personnal_collection_link)
+    # For each hosting_subcription present in the collection
+    for hosting_subscription_link in collection['_links']['item']:
+      if hosting_subscription_link not in self.visited_hosting_subcriptions_link_list:
+        hosting_subscription = self._curl(hosting_subscription_link)
+        self.hosting_subcriptions_dict.update({hosting_subscription['title']:hosting_subscription_link})
+        self.visited_hosting_subcriptions_link_list.append(hosting_subscription_link)
+  
+  def _getRelatedInstanceLink(self, hosting_subscription_title):
+    """
+    Return a list of all related instance_url from an hosting_subscription_title
+    """
+    # Update informations
+    self._update_hosting_subscription_informations()
+    # Get specified hosting_subscription
+    hosting_subscription_link = self.hosting_subcriptions_dict[hosting_subscription_title]
+    hosting_subscription = self._curl(hosting_subscription_link)
+    assert(hosting_subscription_title == hosting_subscription['title'])
+    # Get instance collection related to this hosting_subscription
+    instance_collection_link = hosting_subscription['_links']['http://slapos.org/reg/instance']
+    instance_collection = self._curl(instance_collection_link)
+    related_instance_link_list = []
+    # For each instance present in the collection
+    for instance in instance_collection['_links']['item']:
+      related_instance_link_list.append(instance)
+    return related_instance_link_list
+
+  def getNewsFromInstanceLink(self, instance_link):
+      instance = self._curl(instance_link)
+      news_link = instance['_links']['http://slapos.org/reg/news']
+      return self._curl(news_link)
+
+  def isHostingSubsciptionStatusEqualTo(self, hosting_subscription_title, excepted_news_text):
+    """
+    Return True if all related instance state are equal to status,
+    or False if not or if there is are no related instances.
+    """
+    related_instance_link_list = _getRelatedInstanceLink(hosting_subscription_title)
+    # For each instance present in the collection
+    for instance_link in related_instance_link_list:
+      news = self.getNewsFromInstanceLink(instance_link)
+      if excepted_news_text != news['news'][0]['text']:
+        return False
+    return len(related_instance_link_list) > 0
+
+  def isInstanceReady(self, instance_link, status):
+    """
+    Return True if instance status and instance news text ~looks corresponding.
+    ( use the matching of 'correctly' and 'Instance' and status )
+    """
+    # XXX: SlapOS Master doesn't store any "news" about slave instances. Assume true.
+    if self._curl(instance_link)['slave']:
+      return True
+    text = self.getNewsFromInstanceLink(instance_link)['news'][0]['text']
+    return ('Instance' in text) and ('correctly' in text) and (status in text)
+
+  # check if provided 'status' = status
+  def isHostingSubscriptionReady(self, hosting_subscription_title, status):
+    """
+    Return True if all instance status and instance news text ~looks corresponding.
+    ( use the matching of 'correctly' and 'Instance' and status ).
+    """
+    instance_link_list = self._getRelatedInstanceLink(hosting_subscription_title)
+    for instance_link in instance_link_list:
+      if not self.isInstanceReady(instance_link, status):
+        return False
+    return len(instance_link_list) > 0
+    
+  def isRegisteredHostingSubscription(self, hosting_subscription_title):
+    """
+    Return True if the specified hosting_subscription is present on SlapOSMaster
+    """
+    self._update_hosting_subscription_informations()
+    if self.hosting_subcriptions_dict.get(hosting_subscription_title):
+      return True
+    return False
+
+  def getHostingSubscriptionDict(self):
+    """
+    Return the dict of hosting subcription.
+    """
+    return self.hosting_subcriptions_dict
+
+  def getHostingSubscriptionInformationDict(self, title):
+    """
+    Return a dict with informations about Hosting subscription
+    """
+    related_instance_link_list = self._getRelatedInstanceLink(title)
+    related_instance_link = None
+    # Get root instance
+    for link in related_instance_link_list:
+      instance = self._curl(link)
+      if title == instance['title']:
+        related_instance_link = link
+        break
+    # Return information dict
+    if related_instance_link:
+      related_instance = self._curl(related_instance_link)
+      return {
+        'title': related_instance['title'],
+        'status': related_instance['status'],
+        'software_url': related_instance['_links']['http://slapos.org/reg/release'],
+        'software_type': related_instance['software_type'],
+        'computer_guid': related_instance['sla']['computer_guid']
+      }
+    else:
+      return None
+  
\ No newline at end of file
diff --git a/erp5/util/testnode/UnitTestRunner.py b/erp5/util/testnode/UnitTestRunner.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e1a7481c8fd5c1e525286986ffb62760761eec2
--- /dev/null
+++ b/erp5/util/testnode/UnitTestRunner.py
@@ -0,0 +1,162 @@
+##############################################################################
+#
+# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved.
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly advised to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+from datetime import datetime,timedelta
+import os
+import subprocess
+import sys
+import time
+import glob
+import SlapOSControler
+import json
+import time
+import shutil
+import logging
+import string
+import random
+from ProcessManager import SubprocessError, ProcessManager, CancellationError
+from subprocess import CalledProcessError
+from NodeTestSuite import SlapOSInstance
+from Updater import Updater
+from erp5.util import taskdistribution
+
+class UnitTestRunner():
+  def __init__(self, testnode):
+    self.testnode = testnode
+    self.slapos_controler = SlapOSControler.SlapOSControler(
+                                  self.testnode.working_directory,
+                                  self.testnode.config,
+                                  self.testnode.log)
+
+  def _prepareSlapOS(self, working_directory, slapos_instance, log,
+          create_partition=1, software_path_list=None, **kw):
+    """
+    Launch slapos to build software and partitions
+    """
+    slapproxy_log = os.path.join(self.testnode.config['log_directory'],
+                                  'slapproxy.log')
+    log('Configured slapproxy log to %r' % slapproxy_log)
+    reset_software = slapos_instance.retry_software_count > 10
+    if reset_software:
+      slapos_instance.retry_software_count = 0
+    log('testnode, retry_software_count : %r' % \
+             slapos_instance.retry_software_count)
+    self.slapos_controler.initializeSlapOSControler(slapproxy_log=slapproxy_log,
+       process_manager=self.testnode.process_manager, reset_software=reset_software,
+       software_path_list=software_path_list)
+    self.testnode.process_manager.supervisord_pid_file = os.path.join(\
+         self.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
+    method_list= ["runSoftwareRelease"]
+    if create_partition:
+      method_list.append("runComputerPartition")
+    for method_name in method_list:
+      slapos_method = getattr(self.slapos_controler, method_name)
+      log("Before status_dict = slapos_method(...)")
+      status_dict = slapos_method(self.testnode.config,
+                                  environment=self.testnode.config['environment'],
+                                 )
+      log(status_dict)
+      log("After status_dict = slapos_method(...)")
+      if status_dict['status_code'] != 0:
+         slapos_instance.retry = True
+         slapos_instance.retry_software_count += 1
+         raise SubprocessError(status_dict)
+      else:
+         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.testnode.config['slapos_directory'],
+              test_node_slapos, self.testnode.log, create_partition=0,
+              software_path_list=self.testnode.config.get("software_list"))
+
+  def prepareSlapOSForTestSuite(self, node_test_suite):
+    """
+    Build softwares needed by testsuites
+    """
+    log = self.testnode.log
+    if log is None:
+      log = self.testnode.log
+    return self._prepareSlapOS(node_test_suite.working_directory,
+              node_test_suite, log,
+              software_path_list=[node_test_suite.custom_profile_path])
+
+  def runTestSuite(self, node_test_suite, portal_url, log=None):
+    config = self.testnode.config
+    parameter_list = []
+    run_test_suite_path_list = glob.glob("%s/*/bin/runTestSuite" % \
+        self.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]
+    run_test_suite_revision = node_test_suite.revision
+    # Deal with Shebang size limitation
+    invocation_list = self.testnode._dealShebang(run_test_suite_path)
+    invocation_list.extend([run_test_suite_path,
+                           '--test_suite', node_test_suite.test_suite,
+                           '--revision', node_test_suite.revision,
+                           '--test_suite_title', node_test_suite.test_suite_title,
+                           '--node_quantity', config['node_quantity'],
+                           '--master_url', portal_url])
+    firefox_bin_list = glob.glob("%s/soft/*/parts/firefox/firefox-slapos" % \
+        config["slapos_directory"])
+    if len(firefox_bin_list):
+      parameter_list.append('--firefox_bin')
+    xvfb_bin_list = glob.glob("%s/soft/*/parts/xserver/bin/Xvfb" % \
+        config["slapos_directory"])
+    if len(xvfb_bin_list):
+      parameter_list.append('--xvfb_bin')
+    supported_paramater_set = self.testnode.process_manager.getSupportedParameterSet(
+                           run_test_suite_path, parameter_list)
+    if '--firefox_bin' in supported_paramater_set:
+      invocation_list.extend(["--firefox_bin", firefox_bin_list[0]])
+    if '--xvfb_bin' in supported_paramater_set:
+      invocation_list.extend(["--xvfb_bin", xvfb_bin_list[0]])
+    # TODO : include testnode correction ( b111682f14890bf )
+    if hasattr(node_test_suite,'additional_bt5_repository_id'):
+      additional_bt5_path = os.path.join(
+              node_test_suite.working_directory,
+              node_test_suite.additional_bt5_repository_id)
+      invocation_list.extend(["--bt5_path", additional_bt5_path])
+    # From this point, test runner becomes responsible for updating test
+    # result. We only do cleanup if the test runner itself is not able
+    # to run.
+    SlapOSControler.createFolder(node_test_suite.test_suite_directory,
+                                 clean=True)
+    self.testnode.process_manager.spawn(*invocation_list,
+                          cwd=node_test_suite.test_suite_directory,
+                          log_prefix='runTestSuite', get_output=False)
+
+  def getRelativePathUsage(self):
+    """
+    Used by the method testnode.constructProfile() to know
+    if the software.cfg have to use relative path or not.
+    """
+    return False
diff --git a/erp5/util/testnode/Updater.py b/erp5/util/testnode/Updater.py
index 9c6a4bf85f7e461f1081ba9fb104eefde185a238..644a86da760c4db148e9a4ed4597686ecbd75a4e 100644
--- a/erp5/util/testnode/Updater.py
+++ b/erp5/util/testnode/Updater.py
@@ -32,7 +32,7 @@ import subprocess
 import sys
 import threading
 
-from testnode import SubprocessError
+from ProcessManager import SubprocessError
 
 SVN_UP_REV = re.compile(r'^(?:At|Updated to) revision (\d+).$')
 SVN_CHANGED_REV = re.compile(r'^Last Changed Rev.*:\s*(\d+)', re.MULTILINE)
@@ -103,6 +103,30 @@ class Updater(object):
   def _git(self, *args, **kw):
     return self.spawn(self.git_binary, *args, **kw)['stdout'].strip()
 
+  def git_update_server_info(self):
+    return self._git('update-server-info', '-f')
+
+  def git_create_repository_link(self):
+    """ Create a link in depository to the ".git" directory.
+        ex:
+        for "../erp5/.git"
+        "../erp5/erp5.git"->"../erp5/.git" will be created.
+    """
+    git_repository_path = os.path.join(self.getRepositoryPath(), '.git')
+    name = os.path.basename(os.path.normpath(self.getRepositoryPath()))
+    git_repository_link_path = os.path.join(self.getRepositoryPath(), '%s.git' %name)
+    self.log("checking link %s -> %s.."
+                %(git_repository_link_path,git_repository_path))
+    if ( not os.path.lexists(git_repository_link_path) and \
+         not os.path.exists(git_repository_link_path) ):
+      try:
+        os.symlink(git_repository_path, git_repository_link_path)
+        self.log("link: %s -> %s created"
+                %(git_repository_link_path,git_repository_path))
+      except:
+        self.log("Cannot create link from %s -> %s"
+                %(git_repository_link_path,git_repository_path))
+  
   def _git_find_rev(self, ref):
     try:
       return self._git_cache[ref]
@@ -213,3 +237,4 @@ class Updater(object):
     else:
       raise NotImplementedError
     self._path_list += path_list
+    self.git_update_server_info()
diff --git a/erp5/util/testnode/Utils.py b/erp5/util/testnode/Utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..528c7a2c2cb96abec8a9729b170dfeb3c98cd433
--- /dev/null
+++ b/erp5/util/testnode/Utils.py
@@ -0,0 +1,22 @@
+import sys
+import json
+import shutil
+import string
+from random import choice
+
+def deunicodeData(data):
+  if isinstance(data, list):
+    new_data = []
+    for sub_data in data:
+      new_data.append(deunicodeData(sub_data))
+  elif isinstance(data, unicode):
+    new_data = data.encode('utf8')
+  elif isinstance(data, dict):
+    new_data = {}
+    for key, value in data.iteritems():
+      key = deunicodeData(key)
+      value = deunicodeData(value)
+      new_data[key] = value
+  else:
+    new_data = data
+  return new_data
\ No newline at end of file
diff --git a/erp5/util/testnode/__init__.py b/erp5/util/testnode/__init__.py
index 8b309c7cea2219853bf805e85bd4480fa0e4b49c..c6060d1e3350e1fa5eaa68a863a2cd112b9dc0e7 100644
--- a/erp5/util/testnode/__init__.py
+++ b/erp5/util/testnode/__init__.py
@@ -74,15 +74,17 @@ def main(*args):
   config.optionxform = str
   config.readfp(parsed_argument.configuration_file[0])
   for key in ('slapos_directory','working_directory','test_suite_directory',
-              'log_directory','run_directory','proxy_host','proxy_port',
-              'git_binary','zip_binary','node_quantity','test_node_title',
-              'ipv4_address','ipv6_address','test_suite_master_url',
+              'log_directory','run_directory', 'srv_directory', 'proxy_host',
+              'software_directory',
+              'proxy_port', 'git_binary','zip_binary','node_quantity',
+              'test_node_title', 'ipv4_address','ipv6_address','test_suite_master_url',
               'slapgrid_partition_binary','slapgrid_software_binary',
-              'slapproxy_binary', 'httpd_ip', 'httpd_port'):
+              'slapproxy_binary', 'httpd_ip', 'httpd_port', 'httpd_software_access_port',
+              'computer_id', 'server_url'):
     CONFIG[key] = config.get('testnode',key)
 
   for key in ('slapos_directory', 'working_directory', 'test_suite_directory',
-      'log_directory', 'run_directory'):
+      'log_directory', 'run_directory', 'srv_directory', 'software_directory'):
     d = CONFIG[key]
     if not os.path.isdir(d):
       raise ValueError('Directory %r does not exists.' % d)
@@ -107,6 +109,6 @@ def main(*args):
   if 'software_list' in config.sections():
     CONFIG['software_list'] = filter(None,
         config.get("software_list", "path_list").split(","))
-
+  
   testnode = TestNode(logger.info, CONFIG)
   testnode.run()
diff --git a/erp5/util/testnode/testnode.py b/erp5/util/testnode/testnode.py
index 447e3c3201ae0a85194cb9f25cc4a280bb577b37..b39c93ad25dcd86e088bc9d40c54711ac9ed4bdf 100644
--- a/erp5/util/testnode/testnode.py
+++ b/erp5/util/testnode/testnode.py
@@ -37,11 +37,19 @@ import shutil
 import logging
 import string
 import random
+import Utils
+
+import traceback
+
 from ProcessManager import SubprocessError, ProcessManager, CancellationError
 from subprocess import CalledProcessError
 from Updater import Updater
+from NodeTestSuite import NodeTestSuite, SlapOSInstance
+from ScalabilityTestRunner import ScalabilityTestRunner
+from UnitTestRunner import UnitTestRunner
 from erp5.util import taskdistribution
 
+
 DEFAULT_SLEEP_TIMEOUT = 120 # time in seconds to sleep
 MAX_LOG_TIME = 15 # time in days we should keep logs that we can see through
                   # httd
@@ -56,77 +64,6 @@ class DummyLogger(object):
       'critical', 'fatal'):
        setattr(self, name, func)
 
-class SlapOSInstance(object):
-
-  def __init__(self):
-    self.retry_software_count = 0
-    self.retry = False
-
-  def edit(self, **kw):
-    self.__dict__.update(**kw)
-    self._checkData()
-
-  def _checkData(self):
-    pass
-
-def deunicodeData(data):
-  if isinstance(data, list):
-    new_data = []
-    for sub_data in data:
-      new_data.append(deunicodeData(sub_data))
-  elif isinstance(data, unicode):
-    new_data = data.encode('utf8')
-  elif isinstance(data, dict):
-    new_data = {}
-    for key, value in data.iteritems():
-      key = deunicodeData(key)
-      value = deunicodeData(value)
-      new_data[key] = value
-  return new_data
-
-class NodeTestSuite(SlapOSInstance):
-
-  def __init__(self, reference):
-    super(NodeTestSuite, self).__init__()
-    self.reference = reference
-
-  def edit(self, **kw):
-    super(NodeTestSuite, self).edit(**kw)
-
-  def _checkData(self):
-    if getattr(self, "working_directory", None) is not None:
-      if not(self.working_directory.endswith(os.path.sep + self.reference)):
-        self.working_directory = os.path.join(self.working_directory,
-                                             self.reference)
-      SlapOSControler.createFolder(self.working_directory)
-      self.test_suite_directory = os.path.join(
-                                   self.working_directory, "test_suite")
-      self.custom_profile_path = os.path.join(self.working_directory,
-                                 'software.cfg')
-    if getattr(self, "vcs_repository_list", None) is not None:
-      for vcs_repository in self.vcs_repository_list:
-        buildout_section_id = vcs_repository.get('buildout_section_id', None)
-        repository_id = buildout_section_id or \
-                        vcs_repository.get('url').split('/')[-1].split('.')[0]
-        repository_path = os.path.join(self.working_directory,repository_id)
-        vcs_repository['repository_id'] = repository_id
-        vcs_repository['repository_path'] = repository_path
-
-  def createSuiteLog(self):
-    # /srv/slapgrid/slappartXX/srv/var/log/testnode/az-mlksjfmlk234Sljssdflkj23KSdfslj/suite.log
-    alphabets = string.digits + string.letters
-    rand_part = ''.join(random.choice(alphabets) for i in xrange(32))
-    random_suite_folder_id = '%s-%s' % (self.reference, rand_part)
-    suite_log_directory = os.path.join(self.log_directory,
-                                       random_suite_folder_id)
-    SlapOSControler.createFolders(suite_log_directory)
-    self.suite_log_path = os.path.join(suite_log_directory,
-                                       'suite.log')
-    return self.getSuiteLogPath(), random_suite_folder_id
-
-  def getSuiteLogPath(self):
-    return getattr(self,"suite_log_path", None)
-
 class TestNode(object):
 
   def __init__(self, log, config, max_log_time=MAX_LOG_TIME,
@@ -135,26 +72,29 @@ class TestNode(object):
     self.log = log
     self.config = config or {}
     self.process_manager = ProcessManager(log)
+    self.working_directory = config['working_directory']
     self.node_test_suite_dict = {}
+    self.file_handler = None
     self.max_log_time = max_log_time
     self.max_temp_time = max_temp_time
-    self.file_handler = None
+    self.url_access = "https://[0::0]:0123" # Ipv6 + port of the node
+
 
   def checkOldTestSuite(self,test_suite_data):
     config = self.config
-    installed_reference_set = set(os.listdir(config['working_directory']))
+    installed_reference_set = set(os.listdir(self.working_directory))
     wished_reference_set = set([x['test_suite_reference'] for x in test_suite_data])
     to_remove_reference_set = installed_reference_set.difference(
                                  wished_reference_set)
     for y in to_remove_reference_set:
-      fpath = os.path.join(config['working_directory'],y)
+      fpath = os.path.join(self.working_directory,y)
       self.delNodeTestSuite(y)
       self.log("testnode.checkOldTestSuite, DELETING : %r" % (fpath,))
       if os.path.isdir(fpath):
        shutil.rmtree(fpath)
       else:
        os.remove(fpath)
-
+  
   def getNodeTestSuite(self, reference):
     node_test_suite = self.node_test_suite_dict.get(reference)
     if node_test_suite is None:
@@ -166,7 +106,14 @@ class TestNode(object):
     if self.node_test_suite_dict.has_key(reference):
       self.node_test_suite_dict.pop(reference)
 
-  def constructProfile(self, node_test_suite):
+  def _dealShebang(self,run_test_suite_path):
+    line = open(run_test_suite_path, 'r').readline()
+    invocation_list = []
+    if line[:2] == '#!':
+      invocation_list = line[2:].split()
+    return invocation_list
+
+  def constructProfile(self, node_test_suite, test_type, use_relative_path=False):
     config = self.config
     profile_content = ''
     assert len(node_test_suite.vcs_repository_list), "we must have at least one repository"
@@ -184,21 +131,57 @@ class TestNode(object):
         profile_path_count += 1
         if profile_path_count > 1:
           raise ValueError(PROFILE_PATH_KEY + ' defined more than once')
+
+        # Absolute path to relative path
+        software_config_path = os.path.join(repository_path, profile_path)
+        if use_relative_path :
+          from_path = os.path.join(self.working_directory,
+                                    node_test_suite.reference)
+          software_config_path = os.path.relpath(software_config_path, from_path)
+
+
         profile_content_list.append("""
 [buildout]
 extends = %(software_config_path)s
-""" %  {'software_config_path': os.path.join(repository_path, profile_path)})
+""" %  {'software_config_path': software_config_path})
 
+      # Construct sections
       if not(buildout_section_id is None):
-        profile_content_list.append("""
+        # Absolute path to relative
+        if use_relative_path:
+          from_path = os.path.join(self.working_directory,
+                                    node_test_suite.reference)
+          repository_path = os.path.relpath(repository_path, from_path)
+
+        if test_type=="ScalabilityTest":
+#          updater = Updater(repository_path, git_binary=self.config['git_binary'],
+#          branch = vcs_repository.get('branch','master'), log=self.log, process_manager=self.process_manager)
+#          updater.checkout()
+#          revision = updater.getRevision()[1]
+          all_revision = node_test_suite.revision
+          # from 'sec1=xx-azer,sec2=yy-qwer,..' to [[sec1,azer],[sec2,qwer],..]
+          revision_list = [ [x.split('=')[0],x.split('=')[1].split('-')[1]] for x in all_revision.split(',') ]
+          # from [[sec1,azer],[sec2,qwer],..] to {sec1:azer,sec2:qwer,..}
+          revision_dict = {branch:revision for branch,revision in revision_list}
+          # <obfuscated_url> word is modified by in runner.prepareSlapOSForTestSuite()
+          profile_content_list.append("""
+[%(buildout_section_id)s]
+repository = <obfuscated_url>/%(buildout_section_id)s/%(buildout_section_id)s.git
+revision = %(revision)s
+ignore-ssl-certificate = true
+""" %     {'buildout_section_id': buildout_section_id,
+          'revision': revision_dict[buildout_section_id]})
+        else:
+          profile_content_list.append("""
 [%(buildout_section_id)s]
 repository = %(repository_path)s
 branch = %(branch)s
-""" %  {'buildout_section_id': buildout_section_id,
-   'repository_path' : repository_path,
-   'branch' : vcs_repository.get('branch','master')})
+""" %     {'buildout_section_id': buildout_section_id,
+          'repository_path' : repository_path,
+          'branch' : vcs_repository.get('branch','master')})
     if not profile_path_count:
       raise ValueError(PROFILE_PATH_KEY + ' not defined')
+    # Write file
     custom_profile = open(node_test_suite.custom_profile_path, 'w')
     # sort to have buildout section first
     profile_content_list.sort(key=lambda x: [x, ''][x.startswith('\n[buildout]')])
@@ -276,113 +259,10 @@ branch = %(branch)s
                         revision=revision, log=log,
                         process_manager=self.process_manager)
       updater.checkout()
+      updater.git_update_server_info()
+      updater.git_create_repository_link()
       node_test_suite.revision = test_result.revision
 
-  def _prepareSlapOS(self, working_directory, slapos_instance, log,
-          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)
-    reset_software = slapos_instance.retry_software_count > 10
-    if reset_software:
-      slapos_instance.retry_software_count = 0
-    log('testnode, retry_software_count : %r' % \
-             slapos_instance.retry_software_count)
-    self.slapos_controler = SlapOSControler.SlapOSControler(
-      working_directory, self.config, log)
-    self.slapos_controler.initializeSlapOSControler(slapproxy_log=slapproxy_log,
-       process_manager=self.process_manager, reset_software=reset_software,
-       software_path_list=software_path_list)
-    self.process_manager.supervisord_pid_file = os.path.join(\
-         self.slapos_controler.instance_root, 'var', 'run', 'supervisord.pid')
-    method_list= ["runSoftwareRelease"]
-    if create_partition:
-      method_list.append("runComputerPartition")
-    for method_name in method_list:
-      slapos_method = getattr(self.slapos_controler, method_name)
-      status_dict = slapos_method(self.config,
-                                  environment=self.config['environment'],
-                                 )
-      if status_dict['status_code'] != 0:
-         slapos_instance.retry = True
-         slapos_instance.retry_software_count += 1
-         raise SubprocessError(status_dict)
-      else:
-         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, self.log, create_partition=0,
-              software_path_list=self.config.get("software_list"))
-
-  def prepareSlapOSForTestSuite(self, node_test_suite):
-    log = self.log
-    if log is None:
-      log = self.log
-    return self._prepareSlapOS(node_test_suite.working_directory,
-              node_test_suite, log,
-              software_path_list=[node_test_suite.custom_profile_path])
-
-  def _dealShebang(self,run_test_suite_path):
-    line = open(run_test_suite_path, 'r').readline()
-    invocation_list = []
-    if line[:2] == '#!':
-      invocation_list = line[2:].split()
-    return invocation_list
-
-  def runTestSuite(self, node_test_suite, portal_url, log=None):
-    config = self.config
-    parameter_list = []
-    run_test_suite_path_list = glob.glob("%s/*/bin/runTestSuite" % \
-        self.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]
-    run_test_suite_revision = node_test_suite.revision
-    # Deal with Shebang size limitation
-    invocation_list = self._dealShebang(run_test_suite_path)
-    invocation_list.extend([run_test_suite_path,
-                           '--test_suite', node_test_suite.test_suite,
-                           '--revision', node_test_suite.revision,
-                           '--test_suite_title', node_test_suite.test_suite_title,
-                           '--node_quantity', config['node_quantity'],
-                           '--master_url', portal_url])
-    firefox_bin_list = glob.glob("%s/soft/*/parts/firefox/firefox-slapos" % \
-        config["slapos_directory"])
-    if len(firefox_bin_list):
-      parameter_list.append('--firefox_bin')
-    xvfb_bin_list = glob.glob("%s/soft/*/parts/xserver/bin/Xvfb" % \
-        config["slapos_directory"])
-    if len(xvfb_bin_list):
-      parameter_list.append('--xvfb_bin')
-    supported_paramater_set = self.process_manager.getSupportedParameterSet(
-                           run_test_suite_path, parameter_list)
-    if '--firefox_bin' in supported_paramater_set:
-      invocation_list.extend(["--firefox_bin", firefox_bin_list[0]])
-    if '--xvfb_bin' in supported_paramater_set:
-      invocation_list.extend(["--xvfb_bin", xvfb_bin_list[0]])
-    if hasattr(node_test_suite,'additional_bt5_repository_id'):
-      additional_bt5_path = os.path.join(
-              node_test_suite.working_directory,
-              node_test_suite.additional_bt5_repository_id)
-      invocation_list.extend(["--bt5_path", additional_bt5_path])
-    # From this point, test runner becomes responsible for updating test
-    # result. We only do cleanup if the test runner itself is not able
-    # to run.
-    SlapOSControler.createFolder(node_test_suite.test_suite_directory,
-                                 clean=True)
-    self.process_manager.spawn(*invocation_list,
-                          cwd=node_test_suite.test_suite_directory,
-                          log_prefix='runTestSuite', get_output=False)
-
   def _cleanupLog(self):
     config = self.config
     log_directory = self.config['log_directory']
@@ -442,15 +322,39 @@ branch = %(branch)s
           self.cleanUp(None)
           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))
-          test_suite_json =  test_suite_portal.startTestSuite(config['test_node_title'])
-          test_suite_data = deunicodeData(json.loads(test_suite_json))
+          portal = taskdistribution.TaskDistributionTool(portal_url,
+                                                         logger=DummyLogger(log))
+          self.portal = portal
+          self.test_suite_portal = taskdistribution.TaskDistributor(
+                                                        portal_url,
+                                                        logger=DummyLogger(log))
+          self.test_suite_portal.subscribeNode(node_title=config['test_node_title'],
+                                               computer_guid=config['computer_id'])                  
+          test_suite_json = self.test_suite_portal.startTestSuite(
+                                               node_title=config['test_node_title'],
+                                               computer_guid=config['computer_id'])
+          test_suite_data = Utils.deunicodeData(json.loads(test_suite_json))
           log("Got following test suite data from master : %r" % \
               (test_suite_data,))
-          #Clean-up test suites
+          try:
+            my_test_type = self.test_suite_portal.getTestType()
+          except:
+            log("testnode, error during requesting getTestType() method \
+from the distributor.")
+            raise NotImplementedError
+          # Select runner according to the test type
+          if my_test_type == 'UnitTest':
+            runner = UnitTestRunner(self)
+          elif my_test_type == 'ScalabilityTest':
+            runner = ScalabilityTestRunner(self)
+          else:
+            log("testnode, Runner type not implemented.", my_test_type)
+            raise NotImplementedError
+          log("Type of current test is %s" %(my_test_type,))
+          # master testnode gets test_suites, slaves get nothing
+          runner.prepareSlapOSForTestNode(test_node_slapos)
+          # Clean-up test suites
           self.checkOldTestSuite(test_suite_data)
           for test_suite in test_suite_data:
             remote_test_result_needs_cleanup = False
@@ -460,12 +364,16 @@ branch = %(branch)s
                working_directory=self.config['working_directory'],
                log_directory=self.config['log_directory'])
             node_test_suite.edit(**test_suite)
+            # XXX: temporary hack to prevent empty test_suite
+            if not hasattr(node_test_suite, 'test_suite'):
+              node_test_suite.edit(test_suite='')
             run_software = True
-            # Write our own software.cfg to use the local repository
-            self.constructProfile(node_test_suite)
             # kill processes from previous loop if any
             self.process_manager.killPreviousRun()
             self.getAndUpdateFullRevisionList(node_test_suite)
+            # Write our own software.cfg to use the local repository
+            self.constructProfile(node_test_suite, my_test_type, 
+                                  runner.getRelativePathUsage())
             # Make sure we have local repository
             test_result = portal.createTestResult(node_test_suite.revision, [],
                      config['test_node_title'], False,
@@ -476,13 +384,36 @@ branch = %(branch)s
             if test_result is not None:
               self.registerSuiteLog(test_result, node_test_suite)
               self.checkRevision(test_result,node_test_suite)
+              node_test_suite.edit(test_result=test_result)
               # Now prepare the installation of SlapOS and create instance
-              status_dict = self.prepareSlapOSForTestSuite(node_test_suite)
+              status_dict = runner.prepareSlapOSForTestSuite(node_test_suite)
               # Give some time so computer partitions may start
               # 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)
+              if my_test_type == 'UnitTest':
+                runner.runTestSuite(node_test_suite, portal_url)
+              elif my_test_type == 'ScalabilityTest':
+                error_message = None
+                # A problem is appeared during runTestSuite
+                if status_dict['status_code'] == 1:
+                  error_message = "Software installation too long or error(s) are present during SR install."
+                else:
+                  status_dict = runner.runTestSuite(node_test_suite, portal_url)
+                  # A problem is appeared during runTestSuite
+                  if status_dict['status_code'] == 1:
+                    error_message = status_dict['error_message']
+
+                # If an error is appeared
+                if error_message:
+                  test_result.reportFailure(
+                      stdout=error_message
+                  )
+                  self.log(error_message)
+                  raise ValueError(error_message)
+              else:
+                raise NotImplementedError
+                  
               # break the loop to get latest priorities from master
               break
             self.cleanUp(test_result)
@@ -507,8 +438,10 @@ branch = %(branch)s
           node_test_suite.retry = True
           continue
         except:
-            log("erp5testnode exception", exc_info=sys.exc_info())
-            raise
+          ex_type, ex, tb = sys.exc_info()
+          traceback.print_tb(tb)
+          log("erp5testnode exception", exc_info=sys.exc_info())
+          raise
         now = time.time()
         self.cleanUp(test_result)
         if (now-begin) < 120:
@@ -518,9 +451,11 @@ branch = %(branch)s
     except:
       log("Exception in error handling", exc_info=sys.exc_info())
     finally:
+      if 'tb' in locals():
+        del tb
       # Nice way to kill *everything* generated by run process -- process
       # groups working only in POSIX compilant systems
-      # Exceptions are swallowed during cleanup phas
+      # Exceptions are swallowed during cleanup phase
       log("GENERAL EXCEPTION, QUITING")
       self.cleanUp(test_result)
       log("GENERAL EXCEPTION, QUITING, cleanup finished")