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(
204                str(dumpedDescription) for dumpedDescription in
205                        self._dumpDescription(withCoverage, withNbUse))
206
207    def exportToXML(self, document, domElement=None):
208        if domElement == None:
209            domElement = document.createElement(self.tag)
210
211        self._XMLaddAttributes(domElement)
212
213        for child in self.children :
214            domElement.appendChild(child.exportToXML(document))
215
216        return domElement
217
218    def _XMLaddAttributes(self, domElement):
219        attributes = self._getXMLAttributes()
220
221        coverage = self._getCoverage()
222        if coverage != None :
223            attributes["Coverage"] = self._number2percent(coverage)
224
225        for key, value in attributes.items():
226            domElement.setAttribute(key, value)
227
228    def _getXMLAttributes(self):
229        return {
230                "Name": self.name,
231                "NbUse": str(self.nbUse)
232                }
233
234    def _incNbUse(self):
235        self.nbUse += 1
236
237    def childUsed(self, child):
238        self._incNbUse()
239        # Propagate to parent
240        self._tellParentThatChildUsed()
241
242    def _tellParentThatChildUsed(self):
243        if self.parent :
244            self.parent.childUsed(self)
245
246
247    def parentUsed(self):
248        self._incNbUse()
249        # Propagate to children
250        for child in self.children :
251            child.parentUsed()
252
253    def hasBeenUsed(self):
254        return self.nbUse > 0
255
256    def operationOnChild(self, path, operation):
257
258        if path:
259            return self._operationPropagate(path, operation)
260        else :
261            self.debug("operating on self")
262            return operation(self)
263
264    def _operationPropagate(self, path, operation):
265
266        childName = path.pop(0)
267        child = self.getChildFromName(childName)
268
269        return child.operationOnChild(path, operation)
270
271
272
273    def debug(self, stringOrFunction, level=logging.DEBUG):
274        """Print a debug line on stderr in tree form
275
276        If the debug line is expensive to generate, provide callable
277        object, it will be called if log is enable for this level.
278        This callable object should return the logline string.
279        """
280        if logger.isEnabledFor(level):
281
282            # TODO: use buildin callable if python >= 3.2
283            if hasattr(stringOrFunction, "__call__"):
284                string = stringOrFunction()
285            else:
286                string = stringOrFunction
287
288            rankedLine = DebugRankedLine("%s: %s" % (self, string))
289            self._logDebug(rankedLine, level)
290
291    def _logDebug(self, rankedLine, level):
292
293        if self.parent:
294            self.parent._logDebug(rankedLine.increasedRank(), level)
295        else :
296            logger.log(level, str(rankedLine))
297
298
299
300
301class FromDomElement(Element):
302    def __init__(self, DomElement):
303        self._initFromDom(DomElement)
304        super().__init__(self.name)
305
306
307    def _initFromDom(self, DomElement):
308        self.name = DomElement.getAttribute("Name")
309
310
311
312class DomElementLocation():
313    def __init__(self, classConstructor, path=None):
314        self.classConstructor = classConstructor
315        if path :
316            self.path = path
317        else :
318            self.path = []
319
320        self.path.append(classConstructor.tag)
321
322
323class DomPopulatedElement(Element):
324    """Default child populate
325
326    Look for each dom element with tag specified in self.tag
327    and instantiate it with the dom element
328    """
329    childClasses = []
330
331    def populate(self, dom):
332
333        for childDomElementLocation in self.childClasses :
334
335            self.debug("Looking for child %s in path %s" % (
336                childDomElementLocation.path[-1], childDomElementLocation.path))
337
338            for childDomElement in self._findChildFromTagPath(dom, childDomElementLocation.path) :
339
340                childElement = childDomElementLocation.classConstructor(childDomElement)
341                self.addChild(childElement)
342
343                childElement.populate(childDomElement)
344
345    def _findChildFromTagPath(self, dom, path):
346        if not path :
347            yield dom
348        else :
349            # Copy list
350            path = list(path)
351
352            tag = path.pop(0)
353
354            # Find element with tag
355            self.debug("Going to find elements with tag %s in %s" % (tag, dom))
356            self.debug(lambda: "Nb of solutions: %s" % len(dom.getElementsByTagName(tag)))
357
358            for elementByTag in dom.getElementsByTagName(tag) :
359
360                self.debug("Found element: %s" % elementByTag)
361
362                # If the same tag is found
363                if elementByTag in dom.childNodes :
364
365                    # Yield next level
366                    for element in self._findChildFromTagPath(elementByTag, path) :
367                        yield element
368
369
370class Rule(Element):
371
372    def usedIfApplicable(self, criteria):
373        childApplicability = (child.usedIfApplicable(criteria)
374                for child in self.children)
375
376        isApplicable = self._isApplicable(criteria, childApplicability)
377
378        if isApplicable :
379            self._incNbUse()
380
381        self.debug("Rule applicability: %s" % isApplicable)
382        assert(isApplicable == True or isApplicable == False)
383
384        return isApplicable
385
386
387    def _isApplicable(self, criteria, childApplicability):
388        """Return the rule applicability depending on children applicability.
389
390        If at least one child is applicable, return true"""
391        # Lazy evaluation as in the PFW
392        return all(childApplicability)
393
394
395class CriterionRule(FromDomElement, DomPopulatedElement, Rule):
396    tag = "SelectionCriterionRule"
397    childClasses = []
398    isApplicableOperations = {
399                "Includes" : lambda criterion, value:     criterion.stateIncludes(value),
400                "Excludes" : lambda criterion, value: not criterion.stateIncludes(value),
401                "Is"       : lambda criterion, value:     criterion.stateIs(value),
402                "IsNot"    : lambda criterion, value: not criterion.stateIs(value)
403            }
404
405    def _initFromDom(self, DomElement):
406        self.selectionCriterion = DomElement.getAttribute("SelectionCriterion")
407        self.matchesWhen = DomElement.getAttribute("MatchesWhen")
408        self.value = DomElement.getAttribute("Value")
409        self.name = "%s %s %s" % (self.selectionCriterion, self.matchesWhen, self.value)
410
411        applicableOperationWithoutValue = self.isApplicableOperations[self.matchesWhen]
412        self.isApplicableOperation = lambda criterion: applicableOperationWithoutValue(criterion, self.value)
413
414    def _isApplicable(self, criteria, childApplicability):
415
416        return criteria.operationOnChild([self.selectionCriterion],
417                self.isApplicableOperation)
418
419
420class CompoundRule(FromDomElement, DomPopulatedElement, Rule):
421    """CompoundRule can be of type ALL or ANY"""
422    tag = "CompoundRule"
423    # Declare childClasses but define it at first class instantiation
424    childClasses = None
425
426    def __init__(self, dom):
427        # Define childClasses at first class instantiation
428        if self.childClasses == None :
429            self.childClasses = [DomElementLocation(CriterionRule),
430                    DomElementLocation(CompoundRule)]
431        super().__init__(dom)
432
433    def _initFromDom(self, DomElement):
434
435        type = DomElement.getAttribute("Type")
436        self.ofTypeAll = {"All" : True, "Any" : False}[type]
437        self.name = type
438
439    def _isApplicable(self, criteria, childApplicability):
440        if self.ofTypeAll :
441            applicability = super()._isApplicable(criteria, childApplicability)
442        else:
443            # Lazy evaluation as in the PFW
444            applicability = any(childApplicability)
445
446        return applicability
447
448class RootRule(DomPopulatedElement, Rule):
449    tag = "RootRule"
450    childClasses = [DomElementLocation(CompoundRule)]
451
452    def populate(self, dom):
453        super().populate(dom)
454        self.debug("Children: %s" % self.children)
455        # A configuration can only have one or no rule
456        assert(len(self.children) <= 1)
457
458    def _getCoverageDependanceElements(self):
459        return self._getDescendants()
460
461
462class CriteronStates(Element):
463    """Root of configuration application criterion state"""
464    tag = "CriterionStates"
465
466    def parentUsed(self, criteria):
467        """Add criteria to child if not exist, if exist increase it's nbUse"""
468        self._incNbUse()
469
470        matches = [child for child in self.children if child == criteria]
471
472        assert(len(matches) <= 1)
473
474        if matches :
475            self.debug("Criteria state has already been encounter")
476            currentcriteria = matches[0]
477        else :
478            self.debug("Criteria state has never been encounter, saving it")
479            currentcriteria = criteria
480            self.addChild(criteria)
481
482        currentcriteria.parentUsed()
483
484
485
486class Configuration(FromDomElement, DomPopulatedElement):
487    tag = "Configuration"
488    childClasses = []
489
490    class IneligibleConfigurationAppliedError(CustomError):
491
492        def __init__(self, configuration, criteria):
493            self.configuration = configuration
494            self.criteria = criteria
495
496        def __str__(self):
497
498            return ("Applying ineligible %s, "
499                "rule:\n%s\n"
500                "Criteria current state:\n%s" % (
501                    self.configuration,
502                    self.configuration.rootRule.dump(withCoverage=False, withNbUse=False),
503                    self.criteria.dump(withCoverage=False, withNbUse=False)
504                    ))
505
506    def __init__(self, DomElement):
507        super().__init__(DomElement)
508
509        self.rootRule = RootRule("RootRule")
510        self.addChild(self.rootRule)
511
512        self.criteronStates = CriteronStates("CriterionStates")
513        self.addChild(self.criteronStates)
514
515    def populate(self, dom):
516        # Delegate to rootRule
517        self.rootRule.populate(dom)
518
519    def _getCoverage(self):
520        # Delegate to rootRule
521        return self.rootRule._getCoverage()
522
523    def used(self, criteria):
524
525        self._incNbUse()
526
527        # Propagate use to parents
528        self._tellParentThatChildUsed()
529
530        # Propagate to criterion coverage
531        self.criteronStates.parentUsed(criteria.export())
532
533        # Propagate to rules
534        if not self.rootRule.usedIfApplicable(criteria) :
535
536            self.debug("Applied but rule does not match current "
537                       "criteria (parent: %s) " % self.parent.name,
538                    logging.ERROR)
539
540            raise self.IneligibleConfigurationAppliedError(self, criteria.export())
541
542    def _dumpPropagate(self, withCoverage, withNbUse):
543        self.debug("Going to ask %s for description" % self.rootRule)
544        for dumpedDescription in self.rootRule._dumpDescription(
545                withCoverage=withCoverage,
546                withNbUse=withNbUse) :
547            yield dumpedDescription.increasedRank()
548
549        self.debug("Going to ask %s for description" % self.criteronStates)
550        for dumpedDescription in self.criteronStates._dumpDescription(
551                withCoverage=False,
552                withNbUse=withNbUse) :
553            yield dumpedDescription.increasedRank()
554
555
556class Domain(FromDomElement, DomPopulatedElement):
557    tag = "ConfigurableDomain"
558    childClasses = [DomElementLocation(Configuration, ["Configurations"])]
559
560
561class Domains(DomPopulatedElement):
562    tag = "Domains"
563    childClasses = [DomElementLocation(Domain, ["ConfigurableDomains"])]
564
565
566class RankedLine():
567    def __init__(self, string,
568                stringPrefix="|-- ",
569                rankString="|   ",
570                linePrefix="",
571                lineSuffix="\n"):
572        self.string = string
573        self.rank = 0
574        self.stringPrefix = stringPrefix
575        self.rankString = rankString
576        self.linePrefix = linePrefix
577        self.lineSuffix = lineSuffix
578
579    def increasedRank(self):
580        self.rank += 1
581        return self
582
583    def __str__(self):
584        return self.linePrefix + \
585            self.rank * self.rankString + \
586            self.stringPrefix + \
587            self.string + \
588            self.lineSuffix
589
590class DebugRankedLine(RankedLine):
591
592    def __init__(self, string, lineSuffix=""):
593        super().__init__(string,
594                stringPrefix="",
595                rankString="   ",
596                linePrefix="",
597                lineSuffix=lineSuffix)
598
599
600class CriterionState(Element):
601    tag = "CriterionState"
602    def used(self):
603        self._incNbUse()
604
605
606class Criterion(Element):
607    tag = "Criterion"
608    inclusivenessTranslate = {True: "Inclusive", False: "Exclusive"}
609
610    class ChangeRequestToNonAccessibleState(CustomError):
611        def __init__(self, requestedState, detail):
612            self.requestedState = requestedState
613            self.detail = detail
614
615        def __str__(self):
616            return ("Change request to non accessible state %s. Detail: %s" %
617                (self.requestedState, self.detail))
618
619    def __init__(self, name, isInclusif,
620                stateNamesList, currentStateNamesList,
621                ignoreIntegrity=False):
622        super().__init__(name)
623        self.isInclusif = isInclusif
624
625        for state in stateNamesList :
626            self.addChild(CriterionState(state))
627
628        self.currentState = []
629        self.initStateNamesList = list(currentStateNamesList)
630        self.changeState(self.initStateNamesList, ignoreIntegrity)
631
632    def reset(self):
633        # Set current state as provided at initialisation
634        self.changeState(self.initStateNamesList, ignoreIntegrity=True)
635
636    def changeState(self, subStateNames, ignoreIntegrity=False):
637        self.debug("Changing state from: %s to: %s" % (
638                    list(self._getElementNames(self.currentState)),
639                    subStateNames))
640
641        if not ignoreIntegrity and not self.isIntegre(subStateNames):
642            raise self.ChangeRequestToNonAccessibleState(subStateNames,
643                "An exclusive criterion must have a non empty state")
644
645        newCurrentState = []
646        for subStateName in subStateNames :
647            subState = self.getChildFromName(subStateName)
648            subState.used()
649            newCurrentState.append(subState)
650
651        self.currentState = newCurrentState
652
653        self._incNbUse()
654        self._tellParentThatChildUsed()
655
656    def isIntegre(self, subStateNames):
657        return self.isInclusif or len(subStateNames) == 1
658
659    def childUsed(self, child):
660        self.currentState = child
661        super().childUsed(child)
662
663    def export(self):
664        subStateNames = self._getElementNames(self.currentState)
665        return Criterion(self.name, self.isInclusif, subStateNames, subStateNames,
666            ignoreIntegrity=True)
667
668    def stateIncludes(self, subStateName):
669        subStateCurrentNames = list(self._getElementNames(self.currentState))
670
671        self.debug("Testing if %s is included in %s" % (subStateName, subStateCurrentNames))
672
673        isIncluded = subStateName in subStateCurrentNames
674        self.debug("IsIncluded: %s" % isIncluded)
675
676        return isIncluded
677
678
679    def stateIs(self, subStateNames):
680        if len(self.currentState) != 1 :
681            return False
682        else :
683            return self.stateIncludes(subStateNames)
684
685    def _getXMLAttributes(self):
686        attributes = super()._getXMLAttributes()
687        attributes["Type"] = self.inclusivenessTranslate[self.isInclusif]
688        return attributes
689
690
691class Criteria(Element):
692    tag = "Criteria"
693
694    class DuplicatedCriterionError(DuplicatedChildError):
695        pass
696
697    def export(self):
698        self.debug("Exporting criteria")
699        assert(self.children)
700
701        exported = Criteria(self.name)
702        for child in self.children :
703            exported.addChild(child.export())
704        return exported
705
706    def addChild(self, child):
707        if child in self.children:
708            raise self.DuplicatedCriterionError(self, child)
709        super().addChild(child)
710
711class ConfigAppliedWithoutCriteriaError(CustomError):
712    def __init__(self, configurationName, domainName):
713        self.configurationName = configurationName
714        self.domainName = domainName
715    def __str__(self):
716        return ('Applying configuration "%s" from domain "%s" before declaring criteria' %
717                (self.configurationName, self.domainName))
718
719class ParsePFWlog():
720    MATCH = "match"
721    ACTION = "action"
722
723    class ChangeRequestOnUnknownCriterion(CustomError):
724        def __init__(self, criterion):
725            self.criterion = criterion
726
727        def __str__(self):
728            return ("Change request on an unknown criterion %s." %
729                self.criterion)
730
731    def __init__(self, domains, criteria, ErrorsToIgnore=()):
732
733        self.domains = domains;
734        self.criteria = criteria;
735        self.ErrorsToIgnore = ErrorsToIgnore
736
737        configApplicationRegext = r""".*Applying configuration "(.*)" from domain "([^"]*)"""
738        matchConfigApplicationLine = re.compile(configApplicationRegext).match
739
740        criterionCreationRegext = ", ".join([
741                    r""".*Criterion name: (.*)""",
742                    r"""type kind: (.*)""",
743                    r"""current state: (.*)""",
744                    r"""states: {(.*)}"""
745                ])
746        matchCriterionCreationLine = re.compile(criterionCreationRegext).match
747
748        changingCriterionRegext = r""".*Selection criterion changed event: Criterion name: (.*), 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