Repository: dmwm/wmcore
Issue: JobCreator crashing while processing EventBased ACDC jobs - submit6

Apparently I missed another detail in my previous patch #9131, because there might be MCFakeFiles uploaded to the ACDCServer with a single element in the lumi list. The current patch would empty it, as can be seen in the log below
```
2019-04-11 14:36:29,403:140171761039104:INFO:EventBased:ACDC job created with {'attachments': {}, 'jobgroup': None, 'taskType': 'reprocessing', 'owner': 'pdmvserv', 'fwjr': None, 'id': None, 'jobType': 'Production', 'estimatedJobTime': 30520.721662463704, 'estimatedDiskUsage': 454400.0, 'state_time': 1555011389, 'custom': {}, 'state': 'new', 'location': None, 'workflow': 'vlimant_ACDC0_task_HIG-RunIIFall17wmLHEGS-02728__v1_T_190411_192704_3966', 'cache_dir': None, 'estimatedMemoryUsage': 2300.0, 'fwjr_path': None, 'input_files': [{'runs': set([]), 'minrun': 1L, 'last_event': 0, 'newlocations': set(['T3_US_PuertoRico', 'T2_FI_HIP', 'T2_UK_SGrid_RALPP', 'T3_US_Baylor', 'T3_TW_NTU_HEP', 'T2_KR_KNU', 'T2_RU_SINP', 'T3_US_UMD', 'T3_US_Colorado', 'T1_UK_RAL_Disk', 'T2_TH_CUNSTDA', 'T3_US_Kansas', 'T1_IT_CNAF_Disk', 'T2_IT_Bari', 'T2_US_UCSD', 'T2_RU_IHEP', 'T3_US_Vanderbilt_EC2', 'T3_US_JHU', 'T3_BY_NCPHEP', 'T3_US_TTU', 'T3_US_UCR', 'T3_TW_NCU', 'T2_CH_CSCS', 'T2_UA_KIPT', 'T2_PK_NCP', 'T2_RU_PNPI', 'T3_US_UCD', 'T3_CO_Uniandes', 'T2_FR_IPHC', 'T3_US_OSU', 'T3_GR_IASA_GR', 'T3_US_TAMU', 'T2_IT_Rome', 'T2_UK_London_Brunel', 'T3_IN_PUHEP', 'T3_IT_Trieste', 'T2_EE_Estonia', 'T2_IN_TIFR', 'T2_CN_Beijing', 'T2_US_Florida', 'T3_US_Princeton_ICSE', 'T3_US_Rutgers', 'T2_US_Wisconsin', 'T2_HU_Budapest', 'T2_DE_RWTH', 'T2_BR_SPRACE', 'T2_CH_CERN', 'T2_BR_UERJ', 'T3_MX_Cinvestav', 'T3_US_FNALLPC', 'T3_RU_FIAN', 'T3_US_Cornell', 'T2_ES_IFCA', 'T3_ES_Oviedo', 'T3_US_NotreDame', 'T2_DE_DESY', 'T3_HU_Debrecen', 'T2_US_Caltech', 'T3_FR_IPNL', 'T3_BG_UNI_SOFIA', 'T1_RU_JINR_Disk', 'T3_CN_PKU', 'T2_UK_London_IC', 'T2_US_Nebraska', 'T2_ES_CIEMAT', 'T1_FR_CCIN2P3_Disk', 'T2_KR_KISTI', 'T3_US_FSU', 'T3_KR_UOS', 'T3_IT_Perugia', 'T1_ES_PIC_Disk', 'T2_CH_CERNBOX', 'T2_TR_METU', 'T2_AT_Vienna', 'T2_US_Purdue', 'T3_US_Rice', 'T2_TW_NCHC', 'T2_BE_UCL', 'T3_US_FIT', 'T2_UK_SGrid_Bristol', 'T2_PT_NCG_Lisbon', 'T2_IT_Legnaro', 'T1_DE_KIT_Disk', 'T2_RU_ITEP', 'T3_US_SDSC', 'T3_US_FIU', 'T2_RU_JINR', 'T2_IT_Pisa', 'T2_GR_Ioannina', 'T3_US_MIT', 'T2_MY_UPM_BIRUNI', 'T2_FR_GRIF_IRFU', 'T3_US_UMiss', 'T2_FR_CCIN2P3', 'T2_PL_Warsaw', 'T2_US_MIT', 'T2_BE_IIHE', 'T2_RU_INR', 'T2_PL_Swierk', 'T3_CH_PSI', 'T3_IT_Bologna']), 'lfn': 'MCFakeFile-pdmvserv_task_HIG-RunIIFall17wmLHEGS-02728__v1_T_181024_163235_6575-HIG-RunIIFall17wmLHEGS-02728_0-e1016b623be5254da2c8ff2269c4e903', 'locations': set(['T3_US_PuertoRico', 'T2_FI_HIP', 'T2_UK_SGrid_RALPP', 'T3_US_Baylor', 'T3_TW_NTU_HEP', 'T2_KR_KNU', 'T2_RU_SINP', 'T3_US_UMD', 'T3_US_Colorado', 'T1_UK_RAL_Disk', 'T2_TH_CUNSTDA', 'T3_US_Kansas', 'T1_IT_CNAF_Disk', 'T2_IT_Bari', 'T2_US_UCSD', 'T2_RU_IHEP', 'T3_US_Vanderbilt_EC2', 'T3_US_JHU', 'T3_BY_NCPHEP', 'T3_US_TTU', 'T3_US_UCR', 'T3_TW_NCU', 'T2_CH_CSCS', 'T2_UA_KIPT', 'T2_PK_NCP', 'T2_RU_PNPI', 'T3_US_UCD', 'T3_CO_Uniandes', 'T2_FR_IPHC', 'T3_US_OSU', 'T3_GR_IASA_GR', 'T3_US_TAMU', 'T2_IT_Rome', 'T2_UK_London_Brunel', 'T3_IN_PUHEP', 'T3_IT_Trieste', 'T2_EE_Estonia', 'T2_IN_TIFR', 'T2_CN_Beijing', 'T2_US_Florida', 'T3_US_Princeton_ICSE', 'T3_US_Rutgers', 'T2_US_Wisconsin', 'T2_HU_Budapest', 'T2_DE_RWTH', 'T2_BR_SPRACE', 'T2_CH_CERN', 'T2_BR_UERJ', 'T3_MX_Cinvestav', 'T3_US_FNALLPC', 'T3_RU_FIAN', 'T3_US_Cornell', 'T2_ES_IFCA', 'T3_ES_Oviedo', 'T3_US_NotreDame', 'T2_DE_DESY', 'T3_HU_Debrecen', 'T2_US_Caltech', 'T3_FR_IPNL', 'T3_BG_UNI_SOFIA', 'T1_RU_JINR_Disk', 'T3_CN_PKU', 'T2_UK_London_IC', 'T2_US_Nebraska', 'T2_ES_CIEMAT', 'T1_FR_CCIN2P3_Disk', 'T2_KR_KISTI', 'T3_US_FSU', 'T3_KR_UOS', 'T3_IT_Perugia', 'T1_ES_PIC_Disk', 'T2_CH_CERNBOX', 'T2_TR_METU', 'T2_AT_Vienna', 'T2_US_Purdue', 'T3_US_Rice', 'T2_TW_NCHC', 'T2_BE_UCL', 'T3_US_FIT', 'T2_UK_SGrid_Bristol', 'T2_PT_NCG_Lisbon', 'T2_IT_Legnaro', 'T1_DE_KIT_Disk', 'T2_RU_ITEP', 'T3_US_SDSC', 'T3_US_FIU', 'T2_RU_JINR', 'T2_IT_Pisa', 'T2_GR_Ioannina', 'T3_US_MIT', 'T2_MY_UPM_BIRUNI', 'T2_FR_GRIF_IRFU', 'T3_US_UMiss', 'T2_FR_CCIN2P3', 'T2_PL_Warsaw', 'T2_US_MIT', 'T2_BE_IIHE', 'T2_RU_INR', 'T2_PL_Swierk', 'T3_CH_PSI', 'T3_IT_Bologna']), 'id': 12053720, 'first_event': 0, 'parents': set([]), 'checksums': {}, 'events': 6400, 'merged': True, 'size': 0}], 'task': '/vlimant_ACDC0_task_HIG-RunIIFall17wmLHEGS-02728__v1_T_190411_192704_3966/HIG-RunIIFall17wmLHEGS-02728_0', 'name': 'a8776c29-09e0-43de-9e6b-2edaa49c948f-1190', 'couch_record': None, 'mask': {'LastRun': 1, 'FirstRun': 1, 'inclusivemask': True, 'runAndLumis': {}, 'LastEvent': 8804801, 'FirstEvent': 8804001, 'LastLumi': 10895, 'FirstLumi': 10894}, 'retry_count': 0, 'sandbox': None, 'outcome': 'failure'}
Traceback (most recent call last):
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 414, in algorithm
    self.pollSubscriptions()
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 549, in pollSubscriptions
    wmbsJobGroups = next(jobSplittingFunction)
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 87, in runSplitter
    groups = jobFactory(**splitParams)
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/JobFactory.py", line 94, in __call__
    self.algorithm(*args, **kwargs)
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/EventBased.py", line 115, in algorithm
    deterministicPileup, totalJobs)
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/EventBased.py", line 235, in createACDCJobs
    currentLumi = acdcFile["lumis"][0]
IndexError: list index out of range
2019-04-11 14:36:29,480:140171761039104:ERROR:BaseWorkerThread:Error in worker algorithm (1):
Backtrace:
  <WMComponent.JobCreator.JobCreatorPoller.JobCreatorPoller object at 0x7f7c48341950> <@========== WMException Start ==========@>
Exception Class: JobCreatorException
Message: Failed to execute JobCreator. Error: list index out of range
        ModuleName : WMComponent.JobCreator.JobCreatorPoller
        MethodName : algorithm
        ClassInstance : None
        FileName : /data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py
        ClassName : None
        LineNumber : 435
        ErrorNr : 0
Traceback: 
  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 414, in algorithm
    self.pollSubscriptions()

  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 549, in pollSubscriptions
    wmbsJobGroups = next(jobSplittingFunction)

  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMComponent/JobCreator/JobCreatorPoller.py", line 87, in runSplitter
    groups = jobFactory(**splitParams)

  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/JobFactory.py", line 94, in __call__
    self.algorithm(*args, **kwargs)

  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/EventBased.py", line 115, in algorithm
    deterministicPileup, totalJobs)

  File "/data/srv/wmagent/v1.1.20.patch3/sw/slc7_amd64_gcc630/cms/wmagent/1.1.20.patch3/lib/python2.7/site-packages/WMCore/JobSplitting/EventBased.py", line 235, in createACDCJobs
    currentLumi = acdcFile["lumis"][0]
```

Changed files:
test/python/WMCore_t/WMBS_t/JobSplitting_t/EventBased_t.py, src/python/WMCore/JobSplitting/EventBased.py, test/python/WMCore_t/JobSplitting_t/EventBased_t.py

diff --git a/src/python/WMCore/JobSplitting/EventBased.py b/src/python/WMCore/JobSplitting/EventBased.py
index 5696b06a4..535ef3f7d 100644
--- a/src/python/WMCore/JobSplitting/EventBased.py
+++ b/src/python/WMCore/JobSplitting/EventBased.py
@@ -219,15 +219,16 @@ class EventBased(JobFactory):
         """
         _createACDCJobs_
 
-        Create ACDC production jobs, this are treated differentely
+        Create ACDC production jobs, these are treated differently
         since it is an exact copy of the failed jobs.
         """
+        lumisPerJob = 0  # calculated a bit beyond
         for acdcFile in acdcFileInfo:
             if fakeFile['lfn'] == acdcFile['lfn']:
                 eventsToRun = acdcFile["events"]
                 currentEvent = acdcFile["first_event"]
-                lumisPerJob = 0
-                while eventsToRun:
+                acdcFile["lumis"] = sorted(acdcFile["lumis"])
+                while eventsToRun > 0:
                     ### WARNING: it assumes there will NOT be splitting changes between
                     # the original and the ACDC workflow
                     # Lumis to recovery are not necessarily sequential
@@ -241,15 +242,22 @@ class EventBased(JobFactory):
                         currentEvent = 1
                     if eventsToRun >= eventsPerJob:
                         self.currentJob["mask"].setMaxAndSkipEvents(eventsPerJob, currentEvent)
-                        if fakeFile['lfn'].startswith("MCFakeFile"):
-                            lumisPerJob = int(ceil(float(eventsPerJob) / eventsPerLumi))
-                            self.currentJob["mask"].setMaxAndSkipLumis(lumisPerJob, currentLumi)
                     else:
                         self.currentJob["mask"].setMaxAndSkipEvents(eventsToRun, currentEvent)
-                        if fakeFile['lfn'].startswith("MCFakeFile"):
-                            lumisPerJob = int(ceil(float(eventsToRun) / eventsPerLumi))
-                            self.currentJob["mask"].setMaxAndSkipLumis(lumisPerJob, currentLumi)
-                        eventsToRun = eventsPerJob
+
+                    if fakeFile['lfn'].startswith("MCFakeFile"):
+                        # either a shorter last job or a normal sized one
+                        lumisPerJob = int(ceil(float(min(eventsToRun, eventsPerJob)) / eventsPerLumi))
+                        # I don't like it! but if we want to reduce the memory footprint, we need
+                        # to keep merging MCFakeFiles at DataCollection level. Which requires a dirty
+                        # lumi check - for sequential lumis - in here
+                        while True:
+                            setLumis = set(range(currentLumi, currentLumi + lumisPerJob))
+                            if setLumis.issubset(set(acdcFile["lumis"])):
+                                break
+                            else:
+                                lumisPerJob -= 1
+                        self.currentJob["mask"].setMaxAndSkipLumis(lumisPerJob, currentLumi)
                     jobTime = eventsPerJob * timePerEvent
                     diskRequired = eventsPerJob * sizePerEvent
                     self.currentJob.addResourceEstimates(jobTime=jobTime,
diff --git a/test/python/WMCore_t/JobSplitting_t/EventBased_t.py b/test/python/WMCore_t/JobSplitting_t/EventBased_t.py
index 61dd45ee6..0c539bef6 100644
--- a/test/python/WMCore_t/JobSplitting_t/EventBased_t.py
+++ b/test/python/WMCore_t/JobSplitting_t/EventBased_t.py
@@ -16,6 +16,7 @@ from WMCore.DataStructs.Run import Run
 from WMCore.JobSplitting.SplitterFactory import SplitterFactory
 from WMCore.Services.UUIDLib import makeUUID
 
+
 class EventBasedTest(unittest.TestCase):
     """
     _EventBasedTest_
@@ -75,22 +76,29 @@ class EventBasedTest(unittest.TestCase):
         """
         pass
 
-    def generateFakeMCFile(self, numEvents = 100, firstEvent = 1,
-                           lastEvent = 100, firstLumi = 1, lastLumi = 10):
+    def generateFakeMCFile(self, numEvents=100, firstEvent=1, lastEvent=100,
+                           firstLumi=1, lastLumi=10, existingSub=None):
         #MC comes with only one MCFakeFile
-        singleMCFileset = Fileset(name = "MCTestFileset")
         newFile = File("MCFakeFileTest", size = 1000, events = numEvents)
         newFile.setLocation('se01')
         newFile.addRun(Run(1, *range(firstLumi, lastLumi + 1)))
         newFile["first_event"] = firstEvent
         newFile["last_event"] = lastEvent
-        testWorkflow = Workflow()
-        singleMCFileset.addFile(newFile)
-        singleMCFileSubscription = Subscription(fileset = singleMCFileset,
-                                                workflow = testWorkflow,
-                                                split_algo = "EventBased",
-                                                type = "Production")
-        return singleMCFileSubscription
+
+        if existingSub is None:
+            singleMCFileset = Fileset(name="MCTestFileset")
+            singleMCFileset.addFile(newFile)
+            testWorkflow = Workflow()
+            existingSub = Subscription(fileset=singleMCFileset,
+                                       workflow=testWorkflow,
+                                       split_algo="EventBased",
+                                       type="Production")
+        else:
+            existingSub['fileset'].addFile(newFile)
+
+        return existingSub
+
+
 
     def testNoEvents(self):
         """
@@ -345,6 +353,7 @@ class EventBasedTest(unittest.TestCase):
 
             assert job["mask"]["FirstEvent"] is None, \\
                    "ERROR: Job's first event is incorrect."
+
     def testMCExactEvents(self):
         """
         _testMCExactEvents_
@@ -413,6 +422,7 @@ class EventBasedTest(unittest.TestCase):
                 "Error: Job's first lumi is incorrect.")
         self.assertEqual(len(job["mask"].getRunAndLumis()), 0,
                          "Error: Job's mask has runs and lumis")
+
     def testMC99EventSplit(self):
         """
         _testMC99EventSplit_
@@ -732,6 +742,7 @@ class EventBasedTest(unittest.TestCase):
             self.assertTrue(firstJobCondition or secondJobCondition,
                             "Job mask: %s didn't pass neither of the conditions"
                             % job["mask"])
+
     def testMCEventSplitOver32bit(self):
         """
         _testMCEventSplitOver32bit_
diff --git a/test/python/WMCore_t/WMBS_t/JobSplitting_t/EventBased_t.py b/test/python/WMCore_t/WMBS_t/JobSplitting_t/EventBased_t.py
index 2cbb6151c..e8b4503bd 100644
--- a/test/python/WMCore_t/WMBS_t/JobSplitting_t/EventBased_t.py
+++ b/test/python/WMCore_t/WMBS_t/JobSplitting_t/EventBased_t.py
@@ -8,7 +8,7 @@ Event based splitting test.
 import unittest
 import threading
 import os
-
+import random
 from WMCore.Database.CMSCouch import CouchServer
 from WMCore.DAOFactory import DAOFactory
 from WMCore.DataStructs.Run import Run
@@ -165,6 +165,24 @@ class EventBasedTest(unittest.TestCase):
         self.couchDB.commit()
         return
 
+    def populateACDCFakeFile(self, filesList):
+        """
+        _populateACDCFakeFile_
+
+        Create an ACDC Collection with a MCFakeFile
+        """
+        workflowName = "ACDC_TestEventBased"
+        filesetName = "/%s/Production" % workflowName
+        for f in filesList:
+            acdcDoc = {"collection_name": workflowName,
+                       "collection_type": "ACDC.CollectionTypes.DataCollection",
+                       "files": {f['lfn']: f},
+                       "fileset_name": filesetName}
+            self.couchDB.queue(acdcDoc)
+
+        self.couchDB.commit()
+        return
+
     def generateFakeMCFile(self, numEvents=100, firstEvent=1,
                            lastEvent=100, firstLumi=1, lastLumi=10,
                            index=1, existingSub=None):
@@ -525,7 +543,7 @@ class EventBasedTest(unittest.TestCase):
         Test the ability of the EventBased algorithm of creating
         jobs from ACDC correctly
         """
-        self.populateACDCCouch(numFiles=3, lumisPerJob=4, eventsPerJob=100, numberOfJobs=200)
+        self.populateACDCCouch(numFiles=3, lumisPerJob=5, eventsPerJob=100, numberOfJobs=200)
         mcSubscription = self.generateFakeMCFile(20000, 1, 20001, 1, 800, 0)
         mcSubscription = self.generateFakeMCFile(20000, 1, 20001, 801, 1600, 1, mcSubscription)
         mcSubscription = self.generateFakeMCFile(20000, 1, 20001, 1601, 2400, 2, mcSubscription)
@@ -554,6 +572,63 @@ class EventBasedTest(unittest.TestCase):
 
         return
 
+    def testACDCNonSequential(self):
+        """
+        _testACDCNonSequential_
+
+        Test the ability of the EventBased algorithm to create the proper jobs
+        given job information from the ACDCServer using non-sequential and irregular
+        (diff number of lumis per job) lumi distribution
+        """
+        eventsPerJob = 700
+        eventsPerLumi=200
+        acdcFiles = []
+        # create 4 MCFakeFiles with a single lumi section
+        for lumi in [337, 197, 529, 421]:
+            lfn = "MCFakeFile-some-hash-00000"
+            firstEvent = random.randint(1, 100) * eventsPerJob + 1
+            acdcFile = File(lfn=lfn, size=1024, events=eventsPerJob, locations=self.validLocations,
+                            merged=False, first_event=firstEvent)
+            run = Run(1, *range(lumi, lumi + 1))
+            acdcFile.addRun(run)
+            acdcFiles.append(acdcFile)
+        # create one last entry with more lumis
+        acdcFile = File(lfn=lfn, size=1024, events=eventsPerJob, locations=self.validLocations,
+                        merged=False, first_event=firstEvent)
+        run = Run(1, *range(277, 277+5))
+        acdcFile.addRun(run)
+        acdcFiles.append(acdcFile)
+        self.populateACDCFakeFile(acdcFiles)
+
+        mcSubscription = self.generateFakeMCFile(3500, 1, 0, 1, 1000, 0)
+        splitter = SplitterFactory()
+        jobFactory = splitter(package="WMCore.WMBS",
+                              subscription=mcSubscription)
+
+        jobGroups = jobFactory(events_per_job=eventsPerJob, events_per_lumi=eventsPerLumi,
+                               collectionName="ACDC_TestEventBased",
+                               couchURL=self.couchUrl, couchDB=self.couchDBName,
+                               filesetName="/ACDC_TestEventBased/Production",
+                               performance=self.performanceParams)
+
+        self.assertEqual(1, len(jobGroups))
+        jobGroup = jobGroups[0]
+        self.assertEqual(5, len(jobGroup.jobs))
+
+        for jobNum, lRange in enumerate([[197, 198], [277, 281], [337, 338], [421, 422], [529, 530]]):
+            job = jobGroup.jobs[jobNum]
+            self.assertEqual(1, len(job["input_files"]))
+            mask = job["mask"]
+            self.assertEqual(mask["FirstLumi"], lRange[0])
+            self.assertEqual(mask["LastLumi"], lRange[1])
+            self.assertEqual(eventsPerJob, mask["LastEvent"] - mask["FirstEvent"])
+            self.assertEqual(mask["runAndLumis"], {})
+            self.assertEqual(job["estimatedJobTime"], eventsPerJob * 12)
+            self.assertEqual(job["estimatedDiskUsage"], eventsPerJob * 400)
+            self.assertEqual(job["estimatedMemoryUsage"], 2300)
+
+        return
+
 
 if __name__ == '__main__':
     unittest.main()