• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright (c) 2011-2014, Intel Corporation
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9# 1. Redistributions of source code must retain the above copyright notice, this
10# list of conditions and the following disclaimer.
11#
12# 2. Redistributions in binary form must reproduce the above copyright notice,
13# this list of conditions and the following disclaimer in the documentation and/or
14# other materials provided with the distribution.
15#
16# 3. Neither the name of the copyright holder nor the names of its contributors
17# may be used to endorse or promote products derived from this software without
18# specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
24# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31
32"""
33Generate a coverage report by parsing parameter framework log.
34
35The coverage report contains the:
36 - domain
37 - configuration
38 - rule
39 - criterion
40basic coverage statistics.
41"""
42
43import xml.dom.minidom
44import sys
45import re
46import logging
47
48FORMAT = '%(levelname)s: %(message)s'
49logging.basicConfig(stream=sys.stderr, level=logging.WARNING, format=FORMAT)
50logger = logging.getLogger("Coverage")
51
52class CustomError(Exception):
53    pass
54
55class ChildError(CustomError):
56    def __init__(self, parent, child):
57        self.parent = parent
58        self.child = child
59
60class ChildNotFoundError(ChildError):
61    def __str__(self):
62        return 'Unable to find the child "%s" in "%s"' % (self.child, self.parent)
63
64class DuplicatedChildError(ChildError):
65    def __str__(self):
66        return 'Add existing child "%s" in "%s".' % (self.child, self.parent)
67
68class Element():
69    """Root class for all coverage elements"""
70    tag = "element"
71
72    def __init__(self, name):
73
74        self.parent = None
75        self.children = []
76
77        self.nbUse = 0
78
79        self.name = name
80
81        self.debug("New element")
82
83
84    def __str__(self):
85        return  "%s (%s)" % (self.name, self.tag)
86
87    def __eq__(self, compared):
88        return (self.name == compared.name) and (self.children == compared.children)
89
90    def getName(self, default=""):
91        return self.name or default
92
93    def hasChildren(self):
94        return bool(self.children)
95
96    def getChildren(self):
97        return self.children
98
99    def _getDescendants(self):
100        for child in self.children:
101            yield child
102            for descendant in child._getDescendants():
103                yield descendant
104
105    def getChildFromName(self, childName):
106
107        for child in self.children:
108
109            if child.getName() == childName:
110                return child
111
112        self.debug('Child "%s" not found' % childName, logging.ERROR)
113
114        self.debug("Child list :")
115
116        for child in self.children:
117            self.debug("  - %s" % child)
118
119        raise ChildNotFoundError(self, childName)
120
121
122    def addChild(self, child):
123        self.debug("new child: " + child.name)
124        self.children.append(child)
125        child._adoptedBy(self)
126
127    def _adoptedBy(self, parent):
128        assert(not self.parent)
129        self.parent = parent
130
131    def _getElementNames(self, elementList):
132        return (substate.name for substate in elementList)
133
134    def _description(self, withCoverage, withNbUse):
135        description = self.name
136
137        if withNbUse or withCoverage:
138            description += " has been used " + str(self.nbUse) + " time"
139
140        if withCoverage:
141            description += self._coverageFormating(self._getCoverage())
142
143        return description
144
145
146    def _getCoverage(self):
147        """Return the coverage of the element between 0 and 1
148
149        If the element has no coverage dependency (usually child) return 0 or 1.
150        otherwise the element coverage is the dependency coverage average"""
151        coverageDependanceElements = list(self._getCoverageDependanceElements())
152
153        nbcoverageDependence = len(coverageDependanceElements)
154
155        if nbcoverageDependence == 0:
156            if self.nbUse == 0:
157                return 0
158            else:
159                return 1
160
161        coverageDependenceValues = (depElement._getCoverage()
162                                    for depElement in coverageDependanceElements)
163
164        return sum(coverageDependenceValues) / nbcoverageDependence
165
166    def _getCoverageDependanceElements(self):
167        return self.children
168
169    def _coverageFormating(self, coverage):
170        # If no coverage provided
171        if not coverage:
172            return ""
173
174        # Calculate coverage
175        return " (%s coverage)" % self._number2percent(coverage)
176
177    @staticmethod
178    def _number2percent(number):
179        """Format a number to a integer % string
180
181        example: _number2percent(0.6666) -> "67%"
182        """
183        return "{0:.0f}%".format(100 * number)
184
185
186    def _dumpDescription(self, withCoverage, withNbUse):
187
188        self.debug("yelding description")
189        yield RankedLine(self._description(withCoverage, withNbUse), lineSuffix="")
190
191        for dumped in self._dumpPropagate(withCoverage, withNbUse):
192            yield dumped
193
194    def _dumpPropagate(self, withCoverage, withNbUse):
195
196        for child in self.children:
197            for dumpedDescription in child._dumpDescription(withCoverage, withNbUse):
198                yield dumpedDescription.increasedRank()
199
200
201    def dump(self, withCoverage=False, withNbUse=True):
202
203        return "\n".join(str(dumpedDescription) for dumpedDescription in
204                         self._dumpDescription(withCoverage, withNbUse))
205
206    def exportToXML(self, document, domElement=None):
207        if domElement == None:
208            domElement = document.createElement(self.tag)
209
210        self._XMLaddAttributes(domElement)
211
212        for child in self.children:
213            domElement.appendChild(child.exportToXML(document))
214
215        return domElement
216
217    def _XMLaddAttributes(self, domElement):
218        attributes = self._getXMLAttributes()
219
220        coverage = self._getCoverage()
221        if coverage != None:
222            attributes["Coverage"] = self._number2percent(coverage)
223
224        for key, value in attributes.items():
225            domElement.setAttribute(key, value)
226
227    def _getXMLAttributes(self):
228        return {"Name": self.name,
229                "NbUse": str(self.nbUse)
230               }
231
232    def _incNbUse(self):
233        self.nbUse += 1
234
235    def childUsed(self, child):
236        self._incNbUse()
237        # Propagate to parent
238        self._tellParentThatChildUsed()
239
240    def _tellParentThatChildUsed(self):
241        if self.parent:
242            self.parent.childUsed(self)
243
244
245    def parentUsed(self):
246        self._incNbUse()
247        # Propagate to children
248        for child in self.children:
249            child.parentUsed()
250
251    def hasBeenUsed(self):
252        return self.nbUse > 0
253
254    def operationOnChild(self, path, operation):
255
256        if path:
257            return self._operationPropagate(path, operation)
258        else:
259            self.debug("operating on self")
260            return operation(self)
261
262    def _operationPropagate(self, path, operation):
263
264        childName = path.pop(0)
265        child = self.getChildFromName(childName)
266
267        return child.operationOnChild(path, operation)
268
269
270
271    def debug(self, stringOrFunction, level=logging.DEBUG):
272        """Print a debug line on stderr in tree form
273
274        If the debug line is expensive to generate, provide callable
275        object, it will be called if log is enable for this level.
276        This callable object should return the logline string.
277        """
278        if logger.isEnabledFor(level):
279
280            # TODO: use buildin callable if python >= 3.2
281            if hasattr(stringOrFunction, "__call__"):
282                string = stringOrFunction()
283            else:
284                string = stringOrFunction
285
286            rankedLine = DebugRankedLine("%s: %s" % (self, string))
287            self._logDebug(rankedLine, level)
288
289    def _logDebug(self, rankedLine, level):
290
291        if self.parent:
292            self.parent._logDebug(rankedLine.increasedRank(), level)
293        else:
294            logger.log(level, str(rankedLine))
295
296
297
298
299class FromDomElement(Element):
300    def __init__(self, DomElement):
301        self._initFromDom(DomElement)
302        super().__init__(self.name)
303
304
305    def _initFromDom(self, DomElement):
306        self.name = DomElement.getAttribute("Name")
307
308
309
310class DomElementLocation():
311    def __init__(self, classConstructor, path=None):
312        self.classConstructor = classConstructor
313        if path:
314            self.path = path
315        else:
316            self.path = []
317
318        self.path.append(classConstructor.tag)
319
320
321class DomPopulatedElement(Element):
322    """Default child populate
323
324    Look for each dom element with tag specified in self.tag
325    and instantiate it with the dom element
326    """
327    childClasses = []
328
329    def populate(self, dom):
330
331        for childDomElementLocation in self.childClasses:
332
333            self.debug("Looking for child %s in path %s" % (
334                childDomElementLocation.path[-1], childDomElementLocation.path))
335
336            for childDomElement in self._findChildFromTagPath(dom, childDomElementLocation.path):
337
338                childElement = childDomElementLocation.classConstructor(childDomElement)
339                self.addChild(childElement)
340
341                childElement.populate(childDomElement)
342
343    def _findChildFromTagPath(self, dom, path):
344        if not path:
345            yield dom
346        else:
347            # Copy list
348            path = list(path)
349
350            tag = path.pop(0)
351
352            # Find element with tag
353            self.debug("Going to find elements with tag %s in %s" % (tag, dom))
354            self.debug(lambda: "Nb of solutions: %s" % len(dom.getElementsByTagName(tag)))
355
356            for elementByTag in dom.getElementsByTagName(tag):
357
358                self.debug("Found element: %s" % elementByTag)
359
360                # If the same tag is found
361                if elementByTag in dom.childNodes:
362
363                    # Yield next level
364                    for element in self._findChildFromTagPath(elementByTag, path):
365                        yield element
366
367
368class Rule(Element):
369
370    def usedIfApplicable(self, criteria):
371        childApplicability = (child.usedIfApplicable(criteria)
372                              for child in self.children)
373
374        isApplicable = self._isApplicable(criteria, childApplicability)
375
376        if isApplicable:
377            self._incNbUse()
378
379        self.debug("Rule applicability: %s" % isApplicable)
380        assert(isApplicable == True or isApplicable == False)
381
382        return isApplicable
383
384
385    def _isApplicable(self, criteria, childApplicability):
386        """Return the rule applicability depending on children applicability.
387
388        If at least one child is applicable, return true"""
389        # Lazy evaluation as in the PFW
390        return all(childApplicability)
391
392
393class CriterionRule(FromDomElement, DomPopulatedElement, Rule):
394    tag = "SelectionCriterionRule"
395    childClasses = []
396    isApplicableOperations = {
397        "Includes" : lambda criterion, value: criterion.stateIncludes(value),
398        "Excludes" : lambda criterion, value: not criterion.stateIncludes(value),
399        "Is"       : lambda criterion, value: criterion.stateIs(value),
400        "IsNot"    : lambda criterion, value: not criterion.stateIs(value)
401    }
402
403    def _initFromDom(self, DomElement):
404        self.selectionCriterion = DomElement.getAttribute("SelectionCriterion")
405        self.matchesWhen = DomElement.getAttribute("MatchesWhen")
406        self.value = DomElement.getAttribute("Value")
407        self.name = "%s %s %s" % (self.selectionCriterion, self.matchesWhen, self.value)
408
409        applicableOperationWithoutValue = self.isApplicableOperations[self.matchesWhen]
410        self.isApplicableOperation = lambda criterion: applicableOperationWithoutValue(criterion,
411                                                                                       self.value)
412
413    def _isApplicable(self, criteria, childApplicability):
414
415        return criteria.operationOnChild([self.selectionCriterion], self.isApplicableOperation)
416
417
418class CompoundRule(FromDomElement, DomPopulatedElement, Rule):
419    """CompoundRule can be of type ALL or ANY"""
420    tag = "CompoundRule"
421    # Declare childClasses but define it at first class instantiation
422    childClasses = None
423
424    def __init__(self, dom):
425        # Define childClasses at first class instantiation
426        if self.childClasses == None:
427            self.childClasses = [DomElementLocation(CriterionRule),
428                                 DomElementLocation(CompoundRule)]
429        super().__init__(dom)
430
431    def _initFromDom(self, DomElement):
432
433        type = DomElement.getAttribute("Type")
434        self.ofTypeAll = {"All" : True, "Any" : False}[type]
435        self.name = type
436
437    def _isApplicable(self, criteria, childApplicability):
438        if self.ofTypeAll:
439            applicability = super()._isApplicable(criteria, childApplicability)
440        else:
441            # Lazy evaluation as in the PFW
442            applicability = any(childApplicability)
443
444        return applicability
445
446class RootRule(DomPopulatedElement, Rule):
447    tag = "RootRule"
448    childClasses = [DomElementLocation(CompoundRule)]
449
450    def populate(self, dom):
451        super().populate(dom)
452        self.debug("Children: %s" % self.children)
453        # A configuration can only have one or no rule
454        assert(len(self.children) <= 1)
455
456    def _getCoverageDependanceElements(self):
457        return self._getDescendants()
458
459
460class CriteronStates(Element):
461    """Root of configuration application criterion state"""
462    tag = "CriterionStates"
463
464    def parentUsed(self, criteria):
465        """Add criteria to child if not exist, if exist increase it's nbUse"""
466        self._incNbUse()
467
468        matches = [child for child in self.children if child == criteria]
469
470        assert(len(matches) <= 1)
471
472        if matches:
473            self.debug("Criteria state has already been encounter")
474            currentcriteria = matches[0]
475        else:
476            self.debug("Criteria state has never been encounter, saving it")
477            currentcriteria = criteria
478            self.addChild(criteria)
479
480        currentcriteria.parentUsed()
481
482
483
484class Configuration(FromDomElement, DomPopulatedElement):
485    tag = "Configuration"
486    childClasses = []
487
488    class IneligibleConfigurationAppliedError(CustomError):
489
490        def __init__(self, configuration, criteria):
491            self.configuration = configuration
492            self.criteria = criteria
493
494        def __str__(self):
495
496            return ("Applying ineligible %s, "
497                    "rule:\n%s\n"
498                    "Criteria current state:\n%s" % (
499                        self.configuration,
500                        self.configuration.rootRule.dump(withCoverage=False, withNbUse=False),
501                        self.criteria.dump(withCoverage=False, withNbUse=False)
502                    ))
503
504    def __init__(self, DomElement):
505        super().__init__(DomElement)
506
507        self.rootRule = RootRule("RootRule")
508        self.addChild(self.rootRule)
509
510        self.criteronStates = CriteronStates("CriterionStates")
511        self.addChild(self.criteronStates)
512
513    def populate(self, dom):
514        # Delegate to rootRule
515        self.rootRule.populate(dom)
516
517    def _getCoverage(self):
518        # Delegate to rootRule
519        return self.rootRule._getCoverage()
520
521    def used(self, criteria):
522
523        self._incNbUse()
524
525        # Propagate use to parents
526        self._tellParentThatChildUsed()
527
528        # Propagate to criterion coverage
529        self.criteronStates.parentUsed(criteria.export())
530
531        # Propagate to rules
532        if not self.rootRule.usedIfApplicable(criteria):
533
534            self.debug("Applied but rule does not match current "
535                       "criteria (parent: %s) " % self.parent.name,
536                       logging.ERROR)
537
538            raise self.IneligibleConfigurationAppliedError(self, criteria.export())
539
540    def _dumpPropagate(self, withCoverage, withNbUse):
541        self.debug("Going to ask %s for description" % self.rootRule)
542        for dumpedDescription in self.rootRule._dumpDescription(
543                withCoverage=withCoverage,
544                withNbUse=withNbUse):
545            yield dumpedDescription.increasedRank()
546
547        self.debug("Going to ask %s for description" % self.criteronStates)
548        for dumpedDescription in self.criteronStates._dumpDescription(
549                withCoverage=False,
550                withNbUse=withNbUse):
551            yield dumpedDescription.increasedRank()
552
553
554class Domain(FromDomElement, DomPopulatedElement):
555    tag = "ConfigurableDomain"
556    childClasses = [DomElementLocation(Configuration, ["Configurations"])]
557
558
559class Domains(DomPopulatedElement):
560    tag = "Domains"
561    childClasses = [DomElementLocation(Domain, ["ConfigurableDomains"])]
562
563
564class RankedLine():
565    def __init__(self, string,
566                 stringPrefix="|-- ",
567                 rankString="|   ",
568                 linePrefix="",
569                 lineSuffix="\n"):
570        self.string = string
571        self.rank = 0
572        self.stringPrefix = stringPrefix
573        self.rankString = rankString
574        self.linePrefix = linePrefix
575        self.lineSuffix = lineSuffix
576
577    def increasedRank(self):
578        self.rank += 1
579        return self
580
581    def __str__(self):
582        return self.linePrefix + \
583            self.rank * self.rankString + \
584            self.stringPrefix + \
585            self.string + \
586            self.lineSuffix
587
588class DebugRankedLine(RankedLine):
589
590    def __init__(self, string, lineSuffix=""):
591        super().__init__(string,
592                         stringPrefix="",
593                         rankString="   ",
594                         linePrefix="",
595                         lineSuffix=lineSuffix)
596
597
598class CriterionState(Element):
599    tag = "CriterionState"
600    def used(self):
601        self._incNbUse()
602
603
604class Criterion(Element):
605    tag = "Criterion"
606    inclusivenessTranslate = {True: "Inclusive", False: "Exclusive"}
607
608    class ChangeRequestToNonAccessibleState(CustomError):
609        def __init__(self, requestedState, detail):
610            self.requestedState = requestedState
611            self.detail = detail
612
613        def __str__(self):
614            return ("Change request to non accessible state %s. Detail: %s" %
615                    (self.requestedState, self.detail))
616
617    def __init__(self, name, isInclusif,
618                 stateNamesList, currentStateNamesList,
619                 ignoreIntegrity=False):
620        super().__init__(name)
621        self.isInclusif = isInclusif
622
623        for state in stateNamesList:
624            self.addChild(CriterionState(state))
625
626        self.currentState = []
627        self.initStateNamesList = list(currentStateNamesList)
628        self.changeState(self.initStateNamesList, ignoreIntegrity)
629
630    def reset(self):
631        # Set current state as provided at initialisation
632        self.changeState(self.initStateNamesList, ignoreIntegrity=True)
633
634    def changeState(self, subStateNames, ignoreIntegrity=False):
635        self.debug("Changing state from: %s to: %s" % (
636            list(self._getElementNames(self.currentState)),
637            subStateNames))
638
639        if not ignoreIntegrity and not self.isIntegre(subStateNames):
640            raise self.ChangeRequestToNonAccessibleState(subStateNames,
641                                                         "An exclusive criterion must have a non \
642                                                         empty state")
643
644        newCurrentState = []
645        for subStateName in subStateNames:
646            subState = self.getChildFromName(subStateName)
647            subState.used()
648            newCurrentState.append(subState)
649
650        self.currentState = newCurrentState
651
652        self._incNbUse()
653        self._tellParentThatChildUsed()
654
655    def isIntegre(self, subStateNames):
656        return self.isInclusif or len(subStateNames) == 1
657
658    def childUsed(self, child):
659        self.currentState = child
660        super().childUsed(child)
661
662    def export(self):
663        subStateNames = self._getElementNames(self.currentState)
664        return Criterion(self.name, self.isInclusif, subStateNames, subStateNames,
665                         ignoreIntegrity=True)
666
667    def stateIncludes(self, subStateName):
668        subStateCurrentNames = list(self._getElementNames(self.currentState))
669
670        self.debug("Testing if %s is included in %s" % (subStateName, subStateCurrentNames))
671
672        isIncluded = subStateName in subStateCurrentNames
673        self.debug("IsIncluded: %s" % isIncluded)
674
675        return isIncluded
676
677
678    def stateIs(self, subStateNames):
679        if len(self.currentState) != 1:
680            return False
681        else:
682            return self.stateIncludes(subStateNames)
683
684    def _getXMLAttributes(self):
685        attributes = super()._getXMLAttributes()
686        attributes["Type"] = self.inclusivenessTranslate[self.isInclusif]
687        return attributes
688
689
690class Criteria(Element):
691    tag = "Criteria"
692
693    class DuplicatedCriterionError(DuplicatedChildError):
694        pass
695
696    def export(self):
697        self.debug("Exporting criteria")
698        assert(self.children)
699
700        exported = Criteria(self.name)
701        for child in self.children:
702            exported.addChild(child.export())
703        return exported
704
705    def addChild(self, child):
706        if child in self.children:
707            raise self.DuplicatedCriterionError(self, child)
708        super().addChild(child)
709
710class ConfigAppliedWithoutCriteriaError(CustomError):
711    def __init__(self, configurationName, domainName):
712        self.configurationName = configurationName
713        self.domainName = domainName
714    def __str__(self):
715        return ('Applying configuration "%s" from domain "%s" before declaring criteria' %
716                (self.configurationName, self.domainName))
717
718class ParsePFWlog():
719    MATCH = "match"
720    ACTION = "action"
721
722    class ChangeRequestOnUnknownCriterion(CustomError):
723        def __init__(self, criterion):
724            self.criterion = criterion
725
726        def __str__(self):
727            return ("Change request on an unknown criterion %s." %
728                    self.criterion)
729
730    def __init__(self, domains, criteria, ErrorsToIgnore=()):
731
732        self.domains = domains
733        self.criteria = criteria
734        self.ErrorsToIgnore = ErrorsToIgnore
735
736        configApplicationRegext = r""".*Applying configuration "(.*)" from domain "([^"]*)"""
737        matchConfigApplicationLine = re.compile(configApplicationRegext).match
738
739        criterionCreationRegext = ", ".join([
740            r""".*Criterion name: (.*)""",
741            r"""type kind: (.*)""",
742            r"""current state: (.*)""",
743            r"""states: {(.*)}"""
744        ])
745        matchCriterionCreationLine = re.compile(criterionCreationRegext).match
746
747        changingCriterionRegext = r""".*Selection criterion changed event: Criterion name: (.*), \
748            current state: ([^\n\r]*)"""
749        matchChangingCriterionLine = re.compile(changingCriterionRegext).match
750
751        self.lineLogTypes = [
752            {
753                self.MATCH: matchConfigApplicationLine,
754                self.ACTION: self._configApplication
755            }, {
756                self.MATCH: matchCriterionCreationLine,
757                self.ACTION: self._criterionCreation
758            }, {
759                self.MATCH: matchChangingCriterionLine,
760                self.ACTION: self._changingCriterion
761            }
762        ]
763
764    @staticmethod
765    def _formatCriterionList(liststring, separator):
766        list = liststring.split(separator)
767        if len(list) == 1 and list[0] == "<none>":
768            list = []
769        return list
770
771    def _criterionCreation(self, matchCriterionCreation):
772        # Unpack
773        criterionName, criterionType, currentCriterionStates, criterionStates = matchCriterionCreation.group(1, 2, 3, 4)
774
775        criterionStateList = self._formatCriterionList(criterionStates, ", ")
776
777        criterionIsInclusif = {"exclusive" : False, "inclusive" : True}[criterionType]
778
779        currentcriterionStateList = self._formatCriterionList(currentCriterionStates, "|")
780
781        logger.info("Creating criterion: " + criterionName +
782                    " (" + criterionType + ") " +
783                    " with current state: " + str(currentcriterionStateList) +
784                    ", possible states:" + str(criterionStateList))
785
786        try:
787            self.criteria.addChild(Criterion(
788                criterionName,
789                criterionIsInclusif,
790                criterionStateList,
791                currentcriterionStateList
792            ))
793        except self.criteria.DuplicatedCriterionError as ex:
794            logger.debug(ex)
795            logger.warning("Reseting criterion %s. Did you reset the PFW ?" % criterionName)
796            self.criteria.operationOnChild(
797                [criterionName],
798                lambda criterion: criterion.reset()
799            )
800
801
802
803    def _changingCriterion(self, matchChangingCriterion):
804        # Unpack
805        criterionName, newCriterionSubStateNames = matchChangingCriterion.group(1, 2)
806
807        newCriterionState = self._formatCriterionList(newCriterionSubStateNames, "|")
808
809        logger.info("Changing criterion %s to %s" % (criterionName, newCriterionState))
810
811        path = [criterionName]
812        changeCriterionOperation = lambda criterion: criterion.changeState(newCriterionState)
813        try:
814            self.criteria.operationOnChild(path, changeCriterionOperation)
815        except ChildNotFoundError:
816            raise self.ChangeRequestOnUnknownCriterion(criterionName)
817
818    def _configApplication(self, matchConfig):
819        # Unpack
820        configurationName, domainName = matchConfig.group(1, 2)
821
822        # Check that at least one criterion exist
823        if not self.criteria.hasChildren():
824            logger.error("Applying configuration before declaring criteria")
825            logger.info("Is the log starting at PFW boot ?")
826            raise ConfigAppliedWithoutCriteriaError(configurationName, domainName)
827
828        # Change criterion state
829        path = [domainName, configurationName]
830        usedOperation = lambda element: element.used(self.criteria)
831
832        logger.info("Applying configuration %s from domain %s" % (
833            configurationName, domainName))
834
835        self.domains.operationOnChild(path, usedOperation)
836
837
838    def _digest(self, lineLogType, lineLog):
839
840        match = lineLogType[self.MATCH](lineLog)
841        if match:
842            lineLogType[self.ACTION](match)
843            return True
844        return False
845
846
847    def parsePFWlog(self, lines):
848        for lineNb, lineLog in enumerate(lines, 1): # line number starts at 1
849
850            logger.debug("Parsing line :%s" % lineLog.rstrip())
851
852            digested = (self._digest(lineLogType, lineLog)
853                        for lineLogType in self.lineLogTypes)
854
855            try:
856                success = any(digested)
857
858            # Catch some exception in order to print the current parsing line,
859            # then raise the exception again if not continue of error
860            except CustomError as ex:
861                logger.error('Error raised while parsing line %s: "%s"' %
862                             (lineNb, repr(lineLog)))
863
864                # If exception is a subclass of ErrorsToIgnore, log it and continue
865                # otherwise raise it again.
866                if not issubclass(type(ex), self.ErrorsToIgnore):
867                    raise ex
868                else:
869                    logger.error('Ignoring exception:"%s", '
870                                 'can not guarantee database integrity' % ex)
871            else:
872                if not success:
873                    logger.debug("Line does not match, dropped")
874
875
876class Root(Element):
877    tag = "CoverageReport"
878    def __init__(self, name, dom):
879        super().__init__(name)
880        # Create domain tree
881        self.domains = Domains("Domains")
882        self.domains.populate(dom)
883        self.addChild(self.domains)
884        # Create criterion list
885        self.criteria = Criteria("CriterionRoot")
886        self.addChild(self.criteria)
887
888    def exportToXML(self):
889        """Export tree to an xml document"""
890        impl = xml.dom.minidom.getDOMImplementation()
891        document = impl.createDocument(namespaceURI=None, qualifiedName=self.tag, doctype=None)
892        super().exportToXML(document, document.documentElement)
893
894        return document
895
896# ============================
897# Command line argument parser
898# ============================
899
900
901class ArgumentParser:
902    """class that parse command line arguments with argparse library
903
904    Result of parsing are the class attributes.
905    """
906    levelTranslate = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
907
908    def __init__(self):
909
910        try:
911            # As argparse is only in the stdlib since python 3.2,
912            # testing its availability
913            import argparse
914
915        except ImportError:
916            logger.warning("Unable to import argparse "
917                           "(parser for command-line options and arguments), "
918                           "using default argument values:")
919
920            logger.warning(" - InputFile: stdin")
921            self.inputFile = sys.stdin
922
923            logger.warning(" - OutputFile: stdout")
924            self.outputFile = sys.stdout
925
926            try:
927                self.domainsFile = sys.argv[1]
928            except IndexError as ex:
929                logger.fatal("No domain file provided (first argument)")
930                raise ex
931            else:
932                logger.warning(" - Domain file: " + self.domainsFile)
933
934            logger.warning(" - Output format: xml")
935            self.XMLreport = True
936
937            logger.warning(" - Debug level: error")
938            self.debugLevel = logging.ERROR
939        else:
940
941            myArgParser = argparse.ArgumentParser(description='Generate PFW report')
942
943            myArgParser.add_argument(
944                'domainsFile',
945                type=argparse.FileType('r'),
946                help="the PFW domain XML file"
947            )
948            myArgParser.add_argument(
949                'pfwlog', nargs='?',
950                type=argparse.FileType('r'), default=sys.stdin,
951                help="the PFW log file, default stdin"
952            )
953            myArgParser.add_argument(
954                '-o', '--output',
955                dest="outputFile",
956                type=argparse.FileType('w'), default=sys.stdout,
957                help="the coverage report output file, default stdout"
958            )
959            myArgParser.add_argument(
960                '-v', '--verbose',
961                dest="debugLevel", default=0,
962                action='count',
963                help="print debug warnings from warning (default) to debug (-vv)"
964            )
965
966            outputFormatGroupe = myArgParser.add_mutually_exclusive_group(required=False)
967
968            outputFormatGroupe.add_argument(
969                '--xml',
970                dest="xmlFlag",
971                action='store_true',
972                help=" XML coverage output report"
973            )
974            outputFormatGroupe.add_argument(
975                '--raw',
976                dest="rawFlag",
977                action='store_true',
978                help="raw coverage output report"
979            )
980
981            myArgParser.add_argument(
982                '--ignore-unknown-criterion',
983                dest="unknwonCriterionFlag",
984                action='store_true',
985                help="ignore unknown criterion"
986            )
987
988            myArgParser.add_argument(
989                '--ignore-incoherent-criterion-state',
990                dest="incoherentCriterionFlag",
991                action='store_true',
992                help="ignore criterion transition to incoherent state"
993            )
994
995            myArgParser.add_argument(
996                '--ignore-ineligible-configuration-application',
997                dest="ineligibleConfigurationApplicationFlag",
998                action='store_true',
999                help="ignore application of configuration with a false rule "
1000                "(not applicable configuration)"
1001            )
1002
1003            # Process command line arguments
1004            options = myArgParser.parse_args()
1005
1006            # Mapping to attributes
1007            self.inputFile = options.pfwlog
1008            self.outputFile = options.outputFile
1009            self.domainsFile = options.domainsFile
1010
1011            # Output report in xml if flag not set
1012            self.XMLreport = not options.rawFlag
1013
1014            # Setting logger level
1015            levelCapped = min(options.debugLevel, len(self.levelTranslate) - 1)
1016            self.debugLevel = self.levelTranslate[levelCapped]
1017
1018            # Setting ignore options
1019            errorToIgnore = []
1020            if options.ineligibleConfigurationApplicationFlag:
1021                errorToIgnore.append(Configuration.IneligibleConfigurationAppliedError)
1022
1023            if options.incoherentCriterionFlag:
1024                errorToIgnore.append(Criterion.ChangeRequestToNonAccessibleState)
1025
1026            if options.unknwonCriterionFlag:
1027                errorToIgnore.append(ParsePFWlog.ChangeRequestOnUnknownCriterion)
1028
1029            self.errorToIgnore = tuple(errorToIgnore)
1030
1031
1032
1033def main():
1034
1035    errorDuringLogParsing = -1
1036    errorDuringArgumentParsing = 1
1037
1038    try:
1039        commandLineArguments = ArgumentParser()
1040    except LookupError as ex:
1041        logger.error("Error during argument parsing")
1042        logger.debug(str(ex))
1043        sys.exit(errorDuringArgumentParsing)
1044
1045    # Setting logger level
1046    logger.setLevel(commandLineArguments.debugLevel)
1047    logger.info("Log level set to: %s" %
1048                logging.getLevelName(commandLineArguments.debugLevel))
1049
1050    # Create tree from XML
1051    dom = xml.dom.minidom.parse(commandLineArguments.domainsFile)
1052
1053    # Create element tree
1054    root = Root("DomainCoverage", dom)
1055
1056    # Parse PFW events
1057    parser = ParsePFWlog(root.domains, root.criteria, commandLineArguments.errorToIgnore)
1058
1059    try:
1060        parser.parsePFWlog(commandLineArguments.inputFile.readlines())
1061    except CustomError as ex:
1062        logger.fatal("Error during parsing log file %s: %s" %
1063                     (commandLineArguments.inputFile, ex))
1064        sys.exit(errorDuringLogParsing)
1065
1066    # Output report
1067    outputFile = commandLineArguments.outputFile
1068
1069    if not commandLineArguments.XMLreport:
1070        outputFile.write("%s\n" % root.dump(withCoverage=True, withNbUse=True))
1071    else:
1072        outputFile.write(root.exportToXML().toprettyxml())
1073
1074
1075if __name__ == "__main__":
1076    """ Execute main if the python interpreter is running this module as the main program """
1077    main()
1078
1079