Added a merger module to merge testkit result xml files
authorNicolas Zingilé <n.zingile@gmail.com>
Thu, 11 Sep 2014 13:49:56 +0000 (15:49 +0200)
committerNicolas Zingilé <n.zingile@gmail.com>
Thu, 11 Sep 2014 13:49:56 +0000 (15:49 +0200)
Signed-off-by: Nicolas Zingilé <n.zingile@gmail.com>
testkit-merge [new file with mode: 0755]
testkitmerge/__init__.py [new file with mode: 0644]
testkitmerge/merger.py [new file with mode: 0644]

diff --git a/testkit-merge b/testkit-merge
new file mode 100755 (executable)
index 0000000..113f570
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Authors: 
+#          Nicolas Zingile <n.zingile@gmail.com>
+
+import argparse
+from testkitmerge.merger import *
+
+def main():
+    argparser = argparse.ArgumentParser(description='Tool to merge testkit result xml files')
+    argparser.add_argument('-f', '--files', help='list of testkit xml result files', nargs='+')
+    argparser.add_argument('-o', '--outdir',  help='output directory of final result file', required=True)
+    argparser.add_argument('-n', '--name', help ='name of the final result file', default='result.xml')
+    args = argparser.parse_args()
+    print args
+
+    parser = etree.XMLParser(strip_cdata=False)
+    sourcexmltree = None
+    resultxmltree = None
+
+    for resultxml in args.files:
+       if not os.path.isfile(resultxml):
+           print "Error: the file '" + resultxml + "'doesn't exist !"
+           exit(1)
+       if not resultxml.endswith('.xml'):
+           print "Error: '" + resultxml + "' is not an xml file !"
+           exit(1)
+    for index in range(len(args.files)):
+       sourcexmltree =  etree.parse(args.files[index], parser)
+       resultxmltree = merge_testkitxml(sourcexmltree, resultxmltree)
+       
+    resultxmltree.write(os.path.join(args.outdir, args.name), pretty_print=True, encoding='utf8', method='xml', xml_declaration=True)
+
+if __name__ == "__main__":
+    main()
diff --git a/testkitmerge/__init__.py b/testkitmerge/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/testkitmerge/merger.py b/testkitmerge/merger.py
new file mode 100644 (file)
index 0000000..3870151
--- /dev/null
@@ -0,0 +1,283 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# Authors: 
+#           Nicolas Zingile <n.zingile@gmail.com>
+
+"""Merger module for testkit xml files"""
+
+from lxml import etree
+import os
+
+#------------------------- Global variables -------------------------#
+
+TESTDEFATTRS  = {}
+TESTDEFCHILDS = {"environment" : False, "summary" : False, "suite" : False}
+ENVATTRS      = {"build_id" : False, "device_id" : False, "device_model" : False,
+               "device_name" : False, "host" : False, "lite_version" : False,
+               "manufacturer" : False, "resolution" : False, "screen_size" : False}
+ENVCHILDS     = {"other" : False}
+SUMATTRS      = {"test_plan_name" : False}
+SUMCHILDS     = {"start_at" : True, "end_at" : True}
+SUITEATTRS    = {"name" : True, "launcher" : False}
+SUITECHILDS   = {"set" : True}
+SETATTRS      = {"name" : True, "set_debug_msg" : False}
+SETCHILDS     = {"testcase" : False}
+TCATTRS       = {"component" : True, "execution_type" : True, "id" : True,
+               "name" : False, "priority" : True, "purpose" : True,
+                "result" : True, "status" : True, "type" : True}
+TCCHILDS      = {"description" : False, "categories" : False, "result_info" : False,
+                   "categories" : False}
+DESCATTRS     = {}
+DESCCHILDS    = {"pre_condition" : False, "post_condition" : False, "steps" : False,
+               "notes" : False, "test_script_entry" : True}
+RESINFOATTRS  = {}
+RESINFOCHILDS = {"actual_result" : True, "start" : True, "end" : True,
+               "stdout" : False, "stderr" : False}
+
+class ElementError(Exception):
+    """Custom class to handle the merging exceptions"""
+    pass
+
+def check_element(element, attrdico, childsdico):
+    """Checks a node of the testkit result xml tree.
+
+    Allows to verify the integrity of a testkit result xml node.
+    Check if element contains allowed attributes and if the value of
+    those attributes is filled if it should be.
+    Check if the child nodes of element are allowed and if so, checks
+    if the child node contains text if it should.
+
+    Args:
+       element : Element to check
+       attrdico: A dict that contains information on attributes of element
+               keys: String - allowed attributes of the element
+               values: Booleans indicating if the keys should be filled
+       childsdico: A dict that contains information on sub elements of element
+               keys: String - allowed sub elements of element
+               values: Booleans indicating if the keys should contain text
+    Returns:
+
+    Raises:
+       ElementError
+    """
+    for attrname, attrvalue in element.attrib.items():
+        if attrname in attrdico.keys():
+            if not attrvalue and attrdico.get(attrname):
+                raise ElementError("Attribute '" + attrname + "' of element '" + element.tag
+                + "' is not defined")
+        else:
+            raise ElementError("Attribute '" + attrname + "' is not authorized "
+            + "as an attribute of the '" + element.tag + "' element")
+    for child in list(element):
+        if (child.tag not in childsdico.keys()):
+            raise ElementError("Element '" + child.tag + " should not be a child element of '" + element.tag + "'")
+       elif (not child.text and childsdico.get(child.tag)):
+            raise ElementError("The element '" + child.tag + "' should contain some text")
+
+def create_xmltree():
+    """Creates an ElementTree object.
+    Args:
+
+    Return:
+       An ElementTree object that represents en empty testkit result xml file.
+    """
+    root = etree.Element("test_definition")
+    xmltree = etree.ElementTree(root)
+    print "xml tree created !"
+    
+    return xmltree
+
+def create_envandsum(srcxmltree, destxmltree):
+    """Creates the environment and the summary nodes of
+    an ElementTree object.
+
+    Copy the environment and the summary nodes of srcxmltree in 
+    the destxmltree. The destxmltree only contains the root element.
+
+    Args:
+       srcxmltree: Source xmltree from where we want to copy some nodes.
+       destxmltree: Destination xmltree to augment with some nodes.
+
+    Returns:
+       An ElementTree that partially represents a testkit result xml file.
+    """
+    testdef = destxmltree.getroot()
+    environment = srcxmltree.find("/environment")
+    summary = srcxmltree.find("/summary")
+    testdef.append(environment)
+    testdef.append(summary)
+
+    return destxmltree
+
+def check_testdefinition(xmltree):
+    """Checks the test_definition node of a testkit result xml.
+
+    Checkis that all the sub elements of the test_definition node are present
+    and that integrity of that sub elements is good.
+
+    Args:
+       xmltree: An ElementTree that represents a testkit result xml tree.
+
+    Returns:
+
+    Raises:
+       ElementError
+    """
+    print "- checking test_definition node"
+    testdef = xmltree.getroot()
+    check_element(testdef, TESTDEFATTRS, TESTDEFCHILDS)
+    environment = testdef.find("./environment")
+    summary = testdef.find("summary")
+    if environment is not None:
+       print " -- checking environment node"
+        check_element(environment, ENVATTRS, ENVCHILDS)
+    else:
+       raise ElementError("Element 'test_definition' should contain an 'environment' element")
+    if summary is not None:
+       print " -- checking summary node"
+        check_element(summary, SUMATTRS, SUMCHILDS)
+    else:
+       raise ElementError("Element 'test_definition' should contain a 'summary' element")
+    for asuite in testdef.findall("./suite"):
+       print "- checking suite node : " + asuite.get("name") 
+       check_suite(asuite)
+           
+def check_suite(eltsuite):
+    """Checks the integrity of a suite element.
+
+    Args:
+       eltsuite: A suite element to check
+
+    Returns:
+    """
+    check_element(eltsuite, SUITEATTRS, SUITECHILDS) 
+    for child in list(eltsuite):
+       print "- checking set node : " + child.get("name")
+       check_set(child)
+
+def check_set(eltset):
+    """Checks the integrity of a set element.
+
+    Args:
+       eltset: A set element to check.
+
+    Returns:
+    """
+    check_element(eltset, SETATTRS, SETCHILDS)
+    for child in list (eltset):
+       print "- checking testcase node : " + child.get("id")
+       check_testcase(child)
+
+def check_testcase(eltcase):
+    """Checks the integrity of a testcase element.
+
+    Also verify that the result of the eltcase is present and consistent
+
+    Args:
+       eltcase: A testcase element to check
+
+    Returns:
+
+    Raises:
+       ElementError
+    """
+    print "-- checking result"
+    try:
+       result = eltcase.get("result")
+       actual_result = eltcase.find("./result_info/actual_result").text
+       allowed_results = ["PASS", "FAIL", "N/A"]
+    except AttributeError:
+       raise ElementError("result of the testcase is not valid !")
+    if result not in allowed_results or result != actual_result:
+       raise ElementError("The testcase '" + eltcase.get("id") + "' doesn't have a consistent result")
+    check_element(eltcase, TCATTRS, TCCHILDS)
+    for child in list (eltcase):
+       if child.tag == "description":
+           print "-- checking description node"
+           check_element(child, DESCATTRS, DESCCHILDS)
+       elif child.tag == "result_info":
+           print "-- checking result_info node"
+           check_element(child, RESINFOATTRS, RESINFOCHILDS)
+       elif child.tag == "categories":
+           pass
+       else:
+           raise ElementError("Element '" + child.tag + "' is not allowed")
+
+def solve_conflicts(sourcecase, destcase):
+    """Selects a result when same testcase is encountered in both source and destination
+    testkit xml files.
+
+    The result is chosen according to the following priority : FAIL > N/A > PASS.
+
+    Args:
+       sourcecase: A testcase element of the source testkit xml result file
+       destcase: A testcase element of the destination testkit xml result file
+
+    Returns:
+    """
+    srcresult = sourcecase.get('result')
+    destresult = destcase.get('result')
+    if (srcresult == destresult) or (destresult == 'FAIL') \
+    or (srcresult == 'PASS' and destresult == 'N/A'):
+       pass
+    else:
+       destcase.set('result', srcresult)
+       destcase.find('./result_info/actual_result').text = srcresult
+
+def merge_testkitxml (sourcexmltree, destxmltree=None):
+    """Merge two testkit xml result files.
+
+    Merge the information of sourcexmltree in destxmltree. If destxmltree is
+    not definded, creates a new ElementTree and copy all the information of
+    sourcexmltree in it.
+
+    Args:
+       sourcexmltree: The source ElementTree object that represents the 
+               testkit result xml source file
+       destxmltree: The destination ElementTree object that represents the 
+               testkit result xml destination file
+    Returns:
+       An ElementTree that represents the result of the merging of the sourcexmltree
+       and destxmltree
+    """
+    print "## Checking source xml file ..."
+    check_testdefinition(sourcexmltree)
+    print "source xml file is correct. Ok\n"
+    if destxmltree is None:
+        print "Destination xml file doesn't exist... will be created"
+        destxmltree = create_xmltree()
+        create_envandsum(sourcexmltree, destxmltree)
+    for asuite in sourcexmltree.iter('suite'):
+        destsuite = destxmltree.find("/suite[@name='" + asuite.get('name') + "']")
+        if destsuite is not None:
+            for aset in asuite.iter('set'):
+                destset = destsuite.find("./set[@name='" + aset.get('name') + "']")
+                if destset is not None:
+                    for acase in aset.iter('testcase'):
+                        destcase = destset.find("./testcase[@id='" + acase.get('id') + "']")
+                        if destcase is not None:
+                           solve_conflicts(acase, destcase)
+                        else:
+                            destset.append(acase)
+               else:
+                    destsuite.append(aset)
+        else:
+            destxmltree.getroot().append(asuite)
+
+    return destxmltree