示例#1
0
    def addAttachment(self, id, rev, value, name=None, contentType=None, checksum=None, add_checksum=False):
        """
        Add an attachment stored in value to a document identified by id at revision rev.
        If specified the attachement will be uploaded as name, other wise the attachment is
        named "attachment".

        If not set CouchDB will try to determine contentType and default to text/plain.

        If checksum is specified pass this to CouchDB, it will refuse if the MD5 checksum
        doesn't match the one provided. If add_checksum is True calculate the checksum of
        the attachment and pass that into CouchDB for validation. The checksum should be the
        base64 encoded binary md5 (as returned by hashlib.md5().digest())
        """
        if name is None:
            name = "attachment"
        req_headers = {}

        if add_checksum:
            # calculate base64 encoded MD5
            keyhash = hashlib.md5()
            value_str = str(value) if not isinstance(value, (newstr, newbytes)) else value
            keyhash.update(encodeUnicodeToBytes(value_str))
            content_md5 = base64.b64encode(keyhash.digest())
            req_headers['Content-MD5'] = decodeBytesToUnicode(content_md5) if PY3 else content_md5
        elif checksum:
            req_headers['Content-MD5'] = decodeBytesToUnicode(checksum) if PY3 else checksum
        return self.put('/%s/%s/%s?rev=%s' % (self.name, id, name, rev),
                        value, encode=False,
                        contentType=contentType,
                        incoming_headers=req_headers)
示例#2
0
 def __setitem__(self, key, value):
     """
     make exception look like a dictionary
     """
     # using unicode sandwich pattern
     key = decodeBytesToUnicode(key, "ignore")
     value = decodeBytesToUnicode(value, "ignore")
     self.data[key] = value
示例#3
0
    def _sanitise_input(self, input_args=[], input_kwargs={}, method=None):
        """
        Pull out the necessary input from kwargs (by name) and, failing that,
        pulls out the number required args from args, which assumes the
        arguments are positional.

        _sanitise_input is called automatically if you use the _addMethod/_addDAO
        convenience functions. If you add your method to the methods dictionary
        by hand you should call _sanitise_input explicitly.

        In all but the most basic cases you'll likely want to over-ride this, or
        at least treat its outcome with deep suspicion.

        TODO: Would be nice to loose the method argument and derive it in this method.

        Returns a dictionary of validated, sanitised input data.
        """
        verb = request.method.upper()

        if len(input_args):
            input_args = list(input_args)
        if (len(input_args) + len(input_kwargs)) > len(
                self.methods[verb][method]['args']):
            self.debug('%s to %s expects %s argument(s), got %s' %
                       (verb, method, len(self.methods[verb][method]['args']),
                        (len(input_args) + len(input_kwargs))))
            raise HTTPError(
                400, 'Invalid input: Input arguments failed sanitation.')
        input_data = {}

        # VK, we must read input kwargs/args as string types
        # rather then unicode one. This is important for cx_Oracle
        # driver which will place parameters into binded queries
        # due to mixmatch (string vs unicode) between python and Oracle
        # we must pass string parameters.
        for a in self.methods[verb][method]['args']:
            if a in input_kwargs:
                v = input_kwargs[a]
                input_data[a] = decodeBytesToUnicode(
                    v) if PY3 else encodeUnicodeToBytes(v)
                input_kwargs.pop(a)
            else:
                if len(input_args):
                    v = input_args.pop(0)
                    input_data[a] = decodeBytesToUnicode(
                        v) if PY3 else encodeUnicodeToBytes(v)
        if input_kwargs:
            raise HTTPError(
                400, 'Invalid input: Input arguments failed sanitation.')
        self.debug('%s raw data: %s' % (method, {
            'args': input_args,
            'kwargs': input_kwargs
        }))
        self.debug('%s sanitised input_data: %s' % (method, input_data))
        return self._validate_input(input_data, verb, method)
示例#4
0
    def addInfo(self, **data):
        """
        _addInfo_

        Add key=value information pairs to an
        exception instance
        """
        for key, value in viewitems(data):
            # assumption: value is not iterable (list, dict, tuple, ...)
            # using unicode sandwich pattern
            key = decodeBytesToUnicode(key, "ignore")
            value = decodeBytesToUnicode(value, "ignore")
            self.data[key] = value
        return
示例#5
0
    def parse(self, response):
        """Parse response header and assign class member data"""
        startRegex = r"^HTTP/\d.\d \d{3}"
        continueRegex = r"^HTTP/\d.\d 100"  # Continue: client should continue its request
        replaceRegex = r"^HTTP/\d.\d"

        response = decodeBytesToUnicode(response)

        for row in response.split('\r'):
            row = row.replace('\n', '')
            if not row:
                continue
            if re.search(startRegex, row):
                if re.search(continueRegex, row):
                    continue
                res = re.sub(replaceRegex, "", row).strip()
                status, reason = res.split(' ', 1)
                self.status = int(status)
                self.reason = reason
                continue
            try:
                key, val = row.split(':', 1)
                self.header[key.strip()] = val.strip()
            except:
                pass
示例#6
0
 def decode(self, data):
     """
     decode data to some appropriate format, for now make it a string...
     """
     if PY3:
         return decodeBytesToUnicode(data)
     return data.__str__()
示例#7
0
def execute_command(command, logger, timeout, redirect=True):
    """
    _execute_command_
    Function to manage commands.
    """

    stdout, stderr, rc = None, None, 99999
    if redirect:
        proc = subprocess.Popen(
            command,
            shell=True,
            cwd=os.environ['PWD'],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stdin=subprocess.PIPE,
        )
    else:
        proc = subprocess.Popen(command, shell=True, cwd=os.environ['PWD'])

    t_beginning = time.time()
    while True:
        if proc.poll() is not None:
            break
        seconds_passed = time.time() - t_beginning
        if timeout and seconds_passed > timeout:
            proc.terminate()
            logger.error('Timeout in %s execution.' % command)
            return stdout, rc

        time.sleep(0.1)

    stdout, stderr = proc.communicate()
    stdout = decodeBytesToUnicode(stdout) if PY3 else stdout
    stderr = decodeBytesToUnicode(stderr) if PY3 else stderr
    rc = proc.returncode

    logger.debug(
        'Executing : \n command : %s\n output : %s\n error: %s\n retcode : %s'
        % (command, stdout, stderr, rc))

    return stdout, stderr, rc
示例#8
0
    def listRunLumis(self, dataset=None, block=None):
        """
        It gets a list of DBSRun objects and returns the number of lumisections per run
        DbsRun (RunNumber,
                NumberOfEvents,
                NumberOfLumiSections,
                TotalLuminosity,
                StoreNumber,
                StartOfRungetLong,
                EndOfRun,
                CreationDate,
                CreatedBy,
                LastModificationDate,
                LastModifiedBy
                )
        """
        # Pointless code in python3
        block = decodeBytesToUnicode(block)
        dataset = decodeBytesToUnicode(dataset)

        try:
            if block:
                results = self.dbs.listRuns(block_name=block)
            else:
                results = self.dbs.listRuns(dataset=dataset)
        except dbsClientException as ex:
            msg = "Error in DBSReader.listRuns(%s, %s)\n" % (dataset, block)
            msg += "%s\n" % formatEx3(ex)
            raise DBSReaderError(msg)

        # send runDict format as result, this format is for sync with dbs2 call
        # which has {run_number: num_lumis} but dbs3 call doesn't return num Lumis
        # So it returns {run_number: None}
        # TODO: After DBS2 is completely removed change the return format more sensible one

        runDict = {}
        for x in results:
            for runNumber in x["run_num"]:
                runDict[runNumber] = None
        return runDict
示例#9
0
def runCommand(cmd, shell=True, timeout=None):
    """
    Run generic command

    This is NOT secure and hence NOT recommended
    It does however have the timeout functions built into it
    timeout must be an int
    Note, setting timeout = 0 does nothing!
    """

    if timeout:
        if not isinstance(timeout, int):
            timeout = None
            logging.error(
                "SubprocessAlgo.runCommand expected int timeout, got %s",
                timeout)
        else:
            signal.signal(signal.SIGALRM, alarmHandler)
            signal.alarm(timeout)
    try:
        pipe = subprocess.Popen(cmd,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                shell=shell)
        stdout, stderr = pipe.communicate()
        if PY3:
            stdout = decodeBytesToUnicode(stdout)
            stderr = decodeBytesToUnicode(stderr)
        returnCode = pipe.returncode
    except Alarm:
        msg = "Alarm sounded while running command after %s seconds.\n" % timeout
        msg += "Command: %s\n" % cmd
        msg += "Raising exception"
        logging.error(msg)
        raise SubprocessAlgoException(msg)

    if timeout:
        signal.alarm(0)

    return stdout, stderr, returnCode
示例#10
0
 def decode(self, data):
     """
     decode the data to python from json
     """
     if data:
         decoder = JSONDecoder()
         thunker = JSONThunker()
         if PY3:
             data = decodeBytesToUnicode(data)
         data = decoder.decode(data)
         unthunked = thunker.unthunk(data)
         return unthunked
     return {}
示例#11
0
 def addBasicAuth(self, username, password):
     """Add basic auth headers to request"""
     ## TODO: base64.encodestring is deprecated
     # https://docs.python.org/3.8/library/base64.html#base64.encodestring
     # change to base64.encodebytes after we drop python2
     username = encodeUnicodeToBytes(username)
     password = encodeUnicodeToBytes(password)
     encodedauth = base64.encodestring(b'%s:%s' %
                                       (username, password)).strip()
     if PY3:
         encodedauth = decodeBytesToUnicode(encodedauth)
     auth_string = "Basic %s" % encodedauth
     self.additionalHeaders["Authorization"] = auth_string
示例#12
0
    def getUserAttributes(self):
        """
        _getUserAttributes_
        Retrieve the user's attributes from the voms-proxy-info call.
        """
        vomsProxyInfoCall = subprocess.Popen(["voms-proxy-info", "-fqan"],
                                             stdout = subprocess.PIPE,
                                             stderr = subprocess.PIPE)
        if vomsProxyInfoCall.wait() != 0:
            return None

        stdout, _ = vomsProxyInfoCall.communicate()
        stdout = decodeBytesToUnicode(stdout) if PY3 else stdout
        return stdout[0:-1]
示例#13
0
    def addConfig(self, newConfig, psetHash=None):
        """
        _addConfig_


        """
        # The newConfig parameter is a URL suitable for passing to urlopen.
        with closing(urllib.request.urlopen(newConfig)) as f:
            configString = f.read(-1)
        configMD5 = hashlib.md5(configString).hexdigest()
        if PY3:
            configString = decodeBytesToUnicode(configString)

        self.document['md5_hash'] = configMD5
        self.document['pset_hash'] = psetHash
        self.attachments['configFile'] = configString
        return
示例#14
0
    def unpersist(self, filename, reportname=None):
        """
        _unpersist_

        Load a pickled FWJR from disk.
        """
        if PY3:
            with open(filename, 'rb') as handle:
                self.data = decodeBytesToUnicode(pickle.load(handle))
        else:
            with open(filename, 'r') as handle:
                self.data = pickle.load(handle)

        # old self.report (if it existed) became unattached
        if reportname:
            self.report = getattr(self.data, reportname)

        return
示例#15
0
    def upload(self, url, args, filename):
        """
        _upload_

        Perform a file upload to the dqm server using HTTPS auth with the
        service proxy provided
        """
        ident = "WMAgent python/%d.%d.%d" % sys.version_info[:3]
        uploadProxy = self.step.upload.proxy or os.environ.get(
            'X509_USER_PROXY', None)
        logging.info("Using proxy file: %s", uploadProxy)
        logging.info("Using CA certificate path: %s",
                     os.environ.get('X509_CERT_DIR'))

        msg = "HTTP POST upload arguments:\n"
        for arg in args:
            msg += "  ==> %s: %s\n" % (arg, args[arg])
        logging.info(msg)

        handler = HTTPSAuthHandler(key=uploadProxy, cert=uploadProxy)
        opener = OpenerDirector()
        opener.add_handler(handler)

        # setup the request object
        url = decodeBytesToUnicode(url) if PY3 else encodeUnicodeToBytes(url)
        datareq = Request(url + '/data/put')
        datareq.add_header('Accept-encoding', 'gzip')
        datareq.add_header('User-agent', ident)
        self.marshall(args, {'file': filename}, datareq)

        if 'https://' in url:
            result = opener.open(datareq)
        else:
            opener.add_handler(ProxyHandler({}))
            result = opener.open(datareq)

        data = result.read()
        if result.headers.get('Content-encoding', '') == 'gzip':
            data = GzipFile(fileobj=BytesIO(data)).read()

        return (result.headers, data)
示例#16
0
    def getFileBlockWithParents(self, fileBlockName):
        """
        Retrieve a list of parent files in the block; a flag whether the
        block is still open or not; and it used to resolve the block
        location via PhEDEx.

        :return: a dictionary in the format of:
            {"PhEDExNodeNames" : [],
             "Files" : { LFN : Events },
             "IsOpen" : True|False}
        """
        fileBlockName = decodeBytesToUnicode(fileBlockName)

        if not self.blockExists(fileBlockName):
            msg = "DBSReader.getFileBlockWithParents(%s): No matching data"
            raise DBSReaderError(msg % fileBlockName)

        result = {"PhEDExNodeNames": [],  # FIXME: we better get rid of this line!
                  "Files": self.listFilesInBlockWithParents(fileBlockName),
                  "IsOpen": self.blockIsOpen(fileBlockName)}
        return result
示例#17
0
    def scramArchtoRequiredArch(scramArch=None):
        """
        Converts a given ScramArch to a unique target CPU architecture.
        Note that an architecture precedence is enforced in case there are
        multiple matches.
        In case no scramArch is defined, leave the architecture undefined.
        :param scramArch: can be either a string or a list of ScramArchs
        :return: a string with the matched architecture
        """
        defaultArch = "X86_64"
        requiredArchs = set()
        if scramArch is None:
            return None
        elif isinstance(scramArch, (str, bytes)):
            scramArch = [scramArch]

        for item in scramArch:
            item = decodeBytesToUnicode(item)
            arch = item.split("_")[1]
            if arch not in SCRAM_TO_ARCH:
                msg = "Job configured to a ScramArch: '{}' not supported in BossAir".format(
                    item)
                raise BossAirPluginException(msg)
            requiredArchs.add(SCRAM_TO_ARCH.get(arch))

        # now we have the final list of architectures, return only 1 of them
        if len(requiredArchs) == 1:
            return requiredArchs.pop()
        elif "X86_64" in requiredArchs:
            return "X86_64"
        elif "ppc64le" in requiredArchs:
            return "ppc64le"
        elif "aarch64" in requiredArchs:
            return "aarch64"
        else:  # should never get here!
            return defaultArch
示例#18
0
    def addError(self,
                 stepName,
                 exitCode,
                 errorType,
                 errorDetails,
                 siteName=None):
        """
        _addError_

        Add an error report with an exitCode, type/class of error and
        details of the error as a string.
        Also, report attempted site if error happened before landing on it.
        """
        if self.retrieveStep(stepName) is None:
            # Create a step and set it to failed
            # Assumption: Adding an error fails a step
            self.addStep(stepName, status=1)

        if exitCode is not None:
            exitCode = int(exitCode)

        setExitCodes = self.getStepExitCodes(stepName)
        if exitCode in setExitCodes:
            logging.warning(
                "Exit code: %s has been already added to the job report",
                exitCode)
            return

        stepSection = self.retrieveStep(stepName)
        errorCount = getattr(stepSection.errors, "errorCount", 0)
        errEntry = "error%s" % errorCount
        stepSection.errors.section_(errEntry)
        errDetails = getattr(stepSection.errors, errEntry)
        errDetails.exitCode = exitCode
        errDetails.type = str(errorType)

        try:
            if isinstance(errorDetails, newstr):
                errDetails.details = errorDetails
            elif isinstance(errorDetails, bytes):
                errDetails.details = decodeBytesToUnicode(
                    errorDetails, 'ignore')
            else:
                errDetails.details = newstr(errorDetails)
        except UnicodeEncodeError as ex:
            msg = "Failed to encode the job error details for job ID: %s." % self.getJobID(
            )
            msg += "\nException message: %s\nOriginal error details: %s" % (
                str(ex), errorDetails)
            logging.error(msg)
            msg = "DEFAULT ERROR MESSAGE, because it failed to UTF-8 encode the original message."
            errDetails.details = msg
        except UnicodeDecodeError as ex:
            msg = "Failed to decode the job error details for job ID: %s." % self.getJobID(
            )
            msg += "\nException message: %s\nOriginal error details: %s" % (
                str(ex), errorDetails)
            logging.error(msg)
            msg = "DEFAULT ERROR MESSAGE, because it failed to UTF-8 decode the original message."
            errDetails.details = msg

        setattr(stepSection.errors, "errorCount", errorCount + 1)
        self.setStepStatus(stepName=stepName, status=exitCode)

        if siteName:
            self._setSiteName(site=siteName)

        return
示例#19
0
    def __init__(self, message, errorNo=None, **data):
        self.name = str(self.__class__.__name__)

        # Fix for the unicode encoding issue, see #8056 and #8403
        # interprets this string using utf-8 codec and ignoring any errors.
        # using unicode sandwich pattern
        message = decodeBytesToUnicode(message, "ignore")

        Exception.__init__(self, self.name, message)

        #  //
        # // Init data dictionary with defaults
        # //
        self.data = {}
        self.data.setdefault("ClassName", None)
        self.data.setdefault("ModuleName", None)
        self.data.setdefault("MethodName", None)
        self.data.setdefault("ClassInstance", None)
        self.data.setdefault("FileName", None)
        self.data.setdefault("LineNumber", None)
        if errorNo is None:
            self.data.setdefault("ErrorNr", 0)
        else:
            self.data.setdefault("ErrorNr", errorNo)

        self._message = message
        self.addInfo(**data)

        #  //
        # // Automatically determine the module name
        # //  if not set
        if self.data['ModuleName'] is None:
            try:
                frame = inspect.currentframe()
                lastframe = inspect.getouterframes(frame)[1][0]
                excepModule = inspect.getmodule(lastframe)
                if excepModule is not None:
                    modName = excepModule.__name__
                    self.data['ModuleName'] = modName
            finally:
                frame = None

        # //
        # // Find out where the exception came from
        # //
        try:
            stack = inspect.stack(1)[1]
            self.data['FileName'] = stack[1]
            self.data['LineNumber'] = stack[2]
            self.data['MethodName'] = stack[3]
        finally:
            stack = None

        # //
        # // ClassName if ClassInstance is passed
        # //
        try:
            if self.data['ClassInstance'] is not None:
                self.data['ClassName'] = self.data[
                    'ClassInstance'].__class__.__name__
        except Exception:
            pass

        # Determine the traceback at time of __init__
        try:
            self.traceback = "\n".join(traceback.format_tb(sys.exc_info()[2]))
        except Exception:
            self.traceback = "WMException error: Couldn't get traceback\n"
        # using unicode sandwich pattern
        self.traceback = decodeBytesToUnicode(self.traceback, "ignore")
示例#20
0
    def handlePileup(self):
        """
        _handlePileup_

        Handle pileup settings.
        There has been stored pileup configuration stored in a JSON file
        as a result of DBS querrying when running PileupFetcher,
        this method loads this configuration from sandbox and returns it
        as dictionary.
        The PileupFetcher was called by WorkQueue which creates job's sandbox
        and sandbox gets migrated to the worker node.

        External script iterates over all modules and over all pileup configuration types.
        The only considered types are "data" and "mc" (input to this method).
        If other pileup types are specified by the user, the method doesn't
        modify anything.
        The method considers only files which are present on this local PNN.
        The job will use only those, unless it was told to trust the PU site
        location (trustPUSitelists=True), in this case ALL the blocks/files
        will be added to the PSet and files will be read via AAA.
        Dataset, divided into blocks, may not have all blocks present on a
        particular PNN. However, all files belonging into a block will be
        present when reported by DBS.
        The structure of the pileupDict: PileupFetcher._queryDbsAndGetPileupConfig

        """
        # find out local site SE name
        siteConfig = loadSiteLocalConfig()
        PhEDExNodeName = siteConfig.localStageOut["phedex-node"]
        self.logger.info("Running on site '%s', local PNN: '%s'",
                         siteConfig.siteName, PhEDExNodeName)
        jsonPileupConfig = os.path.join(self.stepSpace.location,
                                        "pileupconf.json")

        # Load pileup json
        try:
            with open(jsonPileupConfig) as jdata:
                pileupDict = json.load(jdata)
        except IOError:
            m = "Could not read pileup JSON configuration file: '%s'" % jsonPileupConfig
            raise RuntimeError(m)

        # Create a json with a list of files and events available
        # after dealing with PhEDEx/AAA logic
        newPileupDict = {}
        fileList = []
        eventsAvailable = 0
        for pileupType in self.step.data.pileup.listSections_():
            pileupType = decodeBytesToUnicode(pileupType)
            useAAA = True if getattr(self.jobBag, 'trustPUSitelists',
                                     False) else False
            self.logger.info("Pileup set to read data remotely: %s", useAAA)
            for blockName in sorted(pileupDict[pileupType].keys()):
                blockDict = pileupDict[pileupType][blockName]
                if PhEDExNodeName in blockDict["PhEDExNodeNames"] or useAAA:
                    eventsAvailable += int(blockDict.get('NumberOfEvents', 0))
                    for fileLFN in blockDict["FileList"]:
                        fileList.append(decodeBytesToUnicode(fileLFN))
            newPileupDict[pileupType] = {
                "eventsAvailable": eventsAvailable,
                "FileList": fileList
            }
        newJsonPileupConfig = os.path.join(self.stepSpace.location,
                                           "CMSSWPileupConfig.json")
        self.logger.info("Generating json for CMSSW pileup script")
        try:
            # If it's a python2 unicode, cmssw_handle_pileup will cast it to str
            with open(newJsonPileupConfig, 'w') as f:
                json.dump(newPileupDict, f)
        except Exception as ex:
            self.logger.exception("Error writing out process filelist json:")
            raise ex

        procScript = "cmssw_handle_pileup.py"
        cmd = "%s --input_pkl %s --output_pkl %s --pileup_dict %s" % (
            procScript, os.path.join(self.stepSpace.location,
                                     self.configPickle),
            os.path.join(self.stepSpace.location,
                         self.configPickle), newJsonPileupConfig)

        if getattr(self.jobBag, "skipPileupEvents", None):
            randomSeed = self.job['task']
            skipPileupEvents = self.jobBag.skipPileupEvents
            cmd += " --skip_pileup_events %s --random_seed %s" % (
                skipPileupEvents, randomSeed)
        self.scramRun(cmd)

        return
示例#21
0
    def testExitCode(self):
        """
        _testExitCode_

        Test and see if we can get an exit code out of a report

        Note: Errors without a return code return 99999
        getStepExitCode: returns the first valid and non-zero exit code
        getExitCode: uses the method above to get an exit code
        getStepExitCodes: returns a set of all exit codes within the step
        """

        report = Report("cmsRun1")
        self.assertEqual(report.getExitCode(), 0)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 0)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {})
        self.assertItemsEqual(report.getStepErrors(stepName="cmsRun1"), {})

        report.addError(stepName="cmsRun1", exitCode=None, errorType="test", errorDetails="test")
        # None is not a valid exitCode, but it will get mapped to 99999
        self.assertEqual(report.getExitCode(), 99999)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 99999)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 1)

        report.addError(stepName="cmsRun1", exitCode=102, errorType="test", errorDetails="test")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 2)

        report.addError(stepName="cmsRun1", exitCode=103, errorType="test", errorDetails="test")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 3)

        # now try to record the same exit code once again
        report.addError(stepName="cmsRun1", exitCode=104, errorType="test", errorDetails="test")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 4)

        # and once again, but different type and details (which does not matter)
        report.addError(stepName="cmsRun1", exitCode=105, errorType="testEE", errorDetails="testAA")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 5)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=106, errorType="test", errorDetails="1 тℯṧт")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 6)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=107, errorType="test", errorDetails="2 тℯṧт \x95")
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106, 107})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 7)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=108, errorType="test", errorDetails=encodeUnicodeToBytes("3 тℯṧт"))
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106, 107, 108})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 8)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=109, errorType="test", errorDetails=decodeBytesToUnicode("4 тℯṧт"))
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106, 107, 108, 109})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 9)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=110, errorType="test", errorDetails={"нεʟʟ◎": 3.14159})
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106, 107, 108, 109, 110})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 10)

        # and once again, but different type and details - testing unicode handling
        report.addError(stepName="cmsRun1", exitCode=111, errorType="test", errorDetails={"нεʟʟ◎ \x95": "ẘøґℓ∂ \x95"})
        self.assertEqual(report.getExitCode(), 102)
        self.assertEqual(report.getStepExitCode(stepName="cmsRun1"), 102)
        self.assertItemsEqual(report.getStepExitCodes(stepName="cmsRun1"), {99999, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111})
        self.assertEqual(report.getStepErrors(stepName="cmsRun1")['errorCount'], 11)