add cmocka tools and fix __WORDSIZE not defined warning

This commit is contained in:
zhangchao53 2024-02-07 16:57:08 +08:00 committed by Xiang Xiao
parent da9903fe57
commit f9f63dc8e1
12 changed files with 1172 additions and 250 deletions

View File

@ -1,249 +0,0 @@
diff --git a/src/cmocka.c b/src/cmocka.c
index ede5b22..ec47f4e 100644
--- a/src/cmocka.c
+++ cmocka/src/cmocka.c
@@ -2532,6 +2532,7 @@ static void cmprintf_group_finish_xml(const char *group_name,
if (fp == NULL) {
fp = fopen(buf, "w");
if (fp != NULL) {
+ xml_printed = 0;
file_append = 1;
file_opened = 1;
} else {
@@ -2554,13 +2555,15 @@ static void cmprintf_group_finish_xml(const char *group_name,
}
if (!xml_printed || (file_opened && !file_append)) {
- fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+ fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<testsuites>\n");
if (!file_opened) {
xml_printed = 1;
}
+ } else {
+ fseek(fp, strlen("</testsuites>\n") * -1, SEEK_END);
+ ftruncate(fileno(fp), ftell(fp));
}
- fprintf(fp, "<testsuites>\n");
fprintf(fp, " <testsuite name=\"%s\" time=\"%.3f\" "
"tests=\"%u\" failures=\"%u\" errors=\"%u\" skipped=\"%u\" >\n",
group_name,
diff --git a/tool/cmocka_implement.py b/tool/cmocka_implement.py
new file mode 100644
index 0000000..11d2842
--- /dev/null
+++ cmocka/tool/cmocka_implement.py
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+import os
+import re
+import typer
+import copy
+import traceback
+
+TESTSUITE_TEMPLATE = """
+/*
+ * Copyright (C) 2023 Xiaomi Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <cmocka.h>
+
+/****************************************************************************
+ * Name: cmocka_{suite_file}_main
+ ****************************************************************************/
+
+int main(int argc, char* argv[])
+{
+
+ /* Add Test Cases */
+ const struct CMUnitTest {suite_name}[] = {
+ cmocka_unit_test_setup_teardown(write case name here, NULL, NULL),
+ };
+
+ /* Run Test cases */
+ cmocka_run_group_tests({suite_name}, NULL, NULL);
+
+ printf("hello cmocka auto-tests\\n");
+
+ return 0;
+}
+"""
+
+TESTCASE_TEMPLATE = """
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+#include <syslog.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <cmocka.h>
+
+
+/****************************************************************************
+ * Name: {case_file}
+ * Description: Testing for scene "describe scene here".
+ * The detail test steps are as following:
+ * 1. describe step 1 here
+ * 2. describe step 2 here
+ * 3. describe step 3 here
+ ****************************************************************************/
+
+void {case_name}(FAR void **state)
+{
+ printf("case: {case_name}\\n");
+ assert(true);
+}
+"""
+
+
+class CmockaGen:
+
+ def __init__(self, path):
+ self.path = path
+ self.suite_path = None
+ self.suite_file = None
+ self.suite_name = None
+ self.case_path = None
+ self.case_file = None
+ self.case_name = None
+
+ def check_path(self):
+ if not self.path:
+ print("request correct path option")
+ return 1
+ if not os.path.exists(self.path):
+ os.makedirs(self.path)
+ return 0
+
+ def check_suite_option(self, suite_option):
+ if not suite_option:
+ return 1
+ opts = suite_option.split("::")
+ if len(opts) != 2:
+ print("suite option must like aaa/bbb/ccc.c::VelaAutoTestSuite")
+ return 1
+ else:
+ self.suite_path = opts[0]
+ self.suite_name = opts[1]
+ paths = self.suite_path.split("/")
+ if not paths[-1].endswith(".c"):
+ print("suite option must like aaa/bbb/ccc.c::VelaAutoTestSuite")
+ return 1
+ else:
+ self.suite_file = paths[-1]
+ return 0
+
+ def generate_suite(self):
+ content = copy.deepcopy(TESTSUITE_TEMPLATE)
+ file_without_ext = self.suite_file.replace(".c", "")
+ content = content.replace("{suite_file}", file_without_ext)
+ content = content.replace("{suite_name}", self.suite_name)
+ full_path = os.path.join(self.path, self.suite_path)
+ dir_path = os.path.dirname(full_path)
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+ with open(full_path, "w") as fl:
+ fl.write(content)
+
+ def check_case_option(self, case_option):
+ if not case_option:
+ return 1
+ opts = case_option.split("::")
+ if len(opts) != 2:
+ print("case option must like aaa/bbb/ccc.c::test_playback_uv_01")
+ return 1
+ else:
+ self.case_path = opts[0]
+ self.case_name = opts[1]
+ if not self.case_name.startswith("test"):
+ print("case function name start with 'test'")
+ return 1
+ paths = self.case_path.split("/")
+ if not paths[-1].endswith(".c"):
+ print("case option must like aaa/bbb/ccc.c::VelaAutoTestcase")
+ return 1
+ file_parts = paths[-1].split("_")
+ pattern = r"[0-9]{2,3}\.c$"
+ if file_parts[0] != "test":
+ print("case file name must start with 'test'")
+ return 1
+ elif not re.search(pattern, file_parts[-1]):
+ print("case file name must end with '00-99' or '000-999'")
+ return 1
+ else:
+ self.case_file = paths[-1]
+ return 0
+
+ def generate_case(self):
+ content = copy.deepcopy(TESTCASE_TEMPLATE)
+ content = content.replace("{case_file}", self.case_file)
+ content = content.replace("{case_name}", self.case_name)
+ full_path = os.path.join(self.path, self.case_path)
+ dir_path = os.path.dirname(full_path)
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+ with open(full_path, "w") as fl:
+ fl.write(content)
+
+ def main(self, suite_option, case_option):
+ try:
+ if self.check_path():
+ return
+ if not self.check_suite_option(suite_option):
+ self.generate_suite()
+ print("generate suite success")
+ if not self.check_case_option(case_option):
+ self.generate_case()
+ print("generate case success")
+ except Exception as e:
+ traceback.print_exc()
+
+
+app = typer.Typer()
+
+@app.command()
+def main(
+ path: str = typer.Option("", help="where to gnerate suite/case file"),
+ suite: str = typer.Option(
+ default="",
+ help="suite file name and suite function name, path/suite::name, eg aaa/bbb/ccc.c::VelaAutoTestcase"
+ ),
+ case: str = typer.Option(
+ default="",
+ help="case file name and case function name, path/case::function, eg ddd/eee/fff.c::test_playback_uv_01"
+ ),
+):
+ """
+ :param path: where to gnerate suite/case file
+ :param suite: suite file name and suite function name
+ :param case: case file name and case function name
+ :return:
+ """
+ gen = CmockaGen(path)
+ gen.main(suite, case)
+
+if __name__ == '__main__':
+ app()

View File

@ -0,0 +1,45 @@
From 875c97edec8143ab66dc73290f8170f8bcd27f6a Mon Sep 17 00:00:00 2001
From: zhangchao53 <zhangchao53@xiaomi.com>
Date: Sun, 10 Sep 2023 16:36:58 +0800
Subject: [PATCH 680/680] Use xml report instead of standard output, support
mutiply testsuite
Change-Id: Ia9f339b76d7e2d9509d4be04cc62b4c3ea6f5fe0
Signed-off-by: zhangchao53 <zhangchao53@xiaomi.com>
---
src/cmocka.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/cmocka.c cmocka/src/cmocka.c
index ede5b22..ec47f4e 100644
--- a/src/cmocka.c
+++ cmocka/src/cmocka.c
@@ -2532,6 +2532,7 @@ static void cmprintf_group_finish_xml(const char *group_name,
if (fp == NULL) {
fp = fopen(buf, "w");
if (fp != NULL) {
+ xml_printed = 0;
file_append = 1;
file_opened = 1;
} else {
@@ -2554,13 +2555,15 @@ static void cmprintf_group_finish_xml(const char *group_name,
}
if (!xml_printed || (file_opened && !file_append)) {
- fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+ fprintf(fp, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<testsuites>\n");
if (!file_opened) {
xml_printed = 1;
}
+ } else {
+ fseek(fp, strlen("</testsuites>\n") * -1, SEEK_END);
+ ftruncate(fileno(fp), ftell(fp));
}
- fprintf(fp, "<testsuites>\n");
fprintf(fp, " <testsuite name=\"%s\" time=\"%.3f\" "
"tests=\"%u\" failures=\"%u\" errors=\"%u\" skipped=\"%u\" >\n",
group_name,
--
2.25.1

View File

@ -0,0 +1,31 @@
From bc244df86faa4ac979009a2738c7bb365ee701e1 Mon Sep 17 00:00:00 2001
From: yintao <yintao@xiaomi.com>
Date: Fri, 6 Jan 2023 02:03:31 +0800
Subject: [PATCH] cmocka/cmocka_private:fix warning in cmocka_private
VELAPLATFO-4452
cmocka/include/cmocka_private.h:102:6:error:__WORDSIZE is not defined
Change-Id: Ib3000a12eddec8a247827a32ce3d998cc3497f4b
Signed-off-by: yintao <yintao@xiaomi.com>
---
include/cmocka_private.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/include/cmocka_private.h b/include/cmocka_private.h
index 4d3ff30..6afab25 100644
--- a/include/cmocka_private.h
+++ b/include/cmocka_private.h
@@ -99,7 +99,7 @@ WINBASEAPI BOOL WINAPI IsDebuggerPresent(VOID);
#else /* _WIN32 */
#ifndef __PRI64_PREFIX
-# if __WORDSIZE == 64
+# if defined(__WORDSIZE) && __WORDSIZE == 64
# define __PRI64_PREFIX "l"
# else
# define __PRI64_PREFIX "ll"
--
2.25.1

View File

@ -44,7 +44,8 @@ cmocka.zip:
$(Q) patch -p0 < 0001-cmocka.c-Reduce-the-call-stack-consumption-of-printf.patch
$(Q) patch -p0 < 0002-cmocka-feature-to-forwarding-cmocka-log-message-to-c.patch
$(Q) patch -p0 < 0003-cmocka-update-method-for-strmatch-to-regex-and-add-list-all-testcases-function.patch
$(Q) patch -p0 < 0004-cmocka-xml-report-and-generate-case-and-suite-tool.patch
$(Q) patch -p0 < 0004-cmocka-xml-report.patch
$(Q) patch -p0 < 0005-cmocka-cmocka_private-fix-warning-in-cmocka_private.patch
context:: cmocka.zip

View File

@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
import copy
import os
import re
import traceback
import typer
TESTSUITE_TEMPLATE = """
/*
* Copyright (C) 2023 Xiaomi Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/****************************************************************************
* Included Files
****************************************************************************/
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <cmocka.h>
/****************************************************************************
* Name: cmocka_{suite_file}_main
****************************************************************************/
int main(int argc, char* argv[])
{
/* Add Test Cases */
const struct CMUnitTest {suite_name}[] = {
cmocka_unit_test_setup_teardown(write case name here, NULL, NULL),
};
/* Run Test cases */
cmocka_run_group_tests({suite_name}, NULL, NULL);
printf("hello cmocka auto-tests\\n");
return 0;
}
"""
TESTCASE_TEMPLATE = """
/****************************************************************************
* Included Files
****************************************************************************/
#include <syslog.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <cmocka.h>
/****************************************************************************
* Name: {case_file}
* Description: Testing for scene "describe scene here".
* The detail test steps are as following:
* 1. describe step 1 here
* 2. describe step 2 here
* 3. describe step 3 here
****************************************************************************/
void {case_name}(FAR void **state)
{
printf("case: {case_name}\\n");
assert(true);
}
"""
class CmockaGen:
def __init__(self, path):
self.path = path
self.suite_path = None
self.suite_file = None
self.suite_name = None
self.case_path = None
self.case_file = None
self.case_name = None
def check_path(self):
if not self.path:
print("request correct path option")
return 1
if not os.path.exists(self.path):
os.makedirs(self.path)
return 0
def check_suite_option(self, suite_option):
if not suite_option:
return 1
opts = suite_option.split("::")
if len(opts) != 2:
print("suite option must like aaa/bbb/ccc.c::VelaAutoTestSuite")
return 1
else:
self.suite_path = opts[0]
self.suite_name = opts[1]
paths = self.suite_path.split("/")
if not paths[-1].endswith(".c"):
print("suite option must like aaa/bbb/ccc.c::VelaAutoTestSuite")
return 1
else:
self.suite_file = paths[-1]
return 0
def generate_suite(self):
content = copy.deepcopy(TESTSUITE_TEMPLATE)
file_without_ext = self.suite_file.replace(".c", "")
content = content.replace("{suite_file}", file_without_ext)
content = content.replace("{suite_name}", self.suite_name)
full_path = os.path.join(self.path, self.suite_path)
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as fl:
fl.write(content)
def check_case_option(self, case_option):
if not case_option:
return 1
opts = case_option.split("::")
if len(opts) != 2:
print("case option must like aaa/bbb/ccc.c::test_playback_uv_01")
return 1
else:
self.case_path = opts[0]
self.case_name = opts[1]
if not self.case_name.startswith("test"):
print("case function name start with 'test'")
return 1
paths = self.case_path.split("/")
if not paths[-1].endswith(".c"):
print("case option must like aaa/bbb/ccc.c::VelaAutoTestcase")
return 1
file_parts = paths[-1].split("_")
pattern = r"[0-9]{2,3}\.c$"
if file_parts[0] != "test":
print("case file name must start with 'test'")
return 1
elif not re.search(pattern, file_parts[-1]):
print("case file name must end with '00-99' or '000-999'")
return 1
else:
self.case_file = paths[-1]
return 0
def generate_case(self):
content = copy.deepcopy(TESTCASE_TEMPLATE)
content = content.replace("{case_file}", self.case_file)
content = content.replace("{case_name}", self.case_name)
full_path = os.path.join(self.path, self.case_path)
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as fl:
fl.write(content)
def main(self, suite_option, case_option):
try:
if self.check_path():
return
if not self.check_suite_option(suite_option):
self.generate_suite()
print("generate suite success")
if not self.check_case_option(case_option):
self.generate_case()
print("generate case success")
except Exception:
traceback.print_exc()
app = typer.Typer()
@app.command()
def main(
path: str = typer.Option("", help="where to gnerate suite/case file"),
suite: str = typer.Option(
default="",
help="suite file name and suite function name, path/suite::name, eg aaa/bbb/ccc.c::VelaAutoTestcase",
),
case: str = typer.Option(
default="",
help="case file name and case function name, path/case::function, eg ddd/eee/fff.c::test_playback_uv_01",
),
):
"""
:param path: where to gnerate suite/case file
:param suite: suite file name and suite function name
:param case: case file name and case function name
:return:
"""
gen = CmockaGen(path)
gen.main(suite, case)
if __name__ == "__main__":
app()

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
############################################################################
# apps/testing/cmocka/tools/cmocka_report.py
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import json
import os
import xml.etree.ElementTree as ET
from enum import Enum
import typer
import xmltodict
from bs4 import BeautifulSoup
from junit2htmlreport.parser import Junit
class ConvertType(str, Enum):
XML2JSON = "xml2json"
XML2HTML = "xml2html"
MERGEXML = "merge"
class CmockaReport:
def __init__(self, xml, out):
self.xml = xml
self.out = out
def xml2dict(self):
"""Parse the XML file and convert it into a dictionary"""
try:
with open(self.xml, "r") as f:
content = f.read()
soup = BeautifulSoup(content, "xml")
xml_dict = xmltodict.parse(str(soup))
return xml_dict
except FileNotFoundError:
print("No such file or directory: {0}".format(self.xml))
except Exception:
print("Failed to parse XML file")
def xml2json(self):
"""Convert XML dictionary into a JSON string"""
xml_dict = self.xml2dict()
if xml_dict is None:
return
json_data = json.dumps(xml_dict, indent=4)
if self.out:
try:
f = open(self.out, "w")
f.write(json_data)
f.close()
print("Job Done")
except FileNotFoundError:
print("No such file or directory: {0}".format(self.out))
except Exception:
print("Failed to write json file")
else:
print(json_data)
def xml2html(self):
"""Convert XML file into a html file"""
if not self.out:
self.out = "{0}.html".format(self.xml.split(".")[0])
try:
report = Junit(self.xml)
html = report.html()
f = open(self.out, "wb")
f.write(html.encode("UTF-8"))
f.close()
print("Job Done")
except FileNotFoundError:
print("No such file: {0}".format(self.out))
except Exception:
print("Failed to write html file")
def mergexml(self):
"""Merge multiple XML files into one"""
merged = ET.Element("testsuites")
for _ in os.listdir(self.xml):
if _.endswith(".xml"):
try:
tree = ET.parse(os.path.join(self.xml, _))
root = tree.getroot()
merged.extend(list(root))
except ET.ParseError as e:
print("Error parsing XML:", _, e)
return
if not merged:
print("Can not find any xml file")
return
if self.out:
try:
ET.ElementTree(merged).write(
self.out, encoding="UTF-8", xml_declaration=True
)
print("Job Done")
except FileNotFoundError:
print("No such file or directory: {0}".format(self.out))
except Exception:
print("Failed to write merge xml file")
else:
ET.dump(merged)
app = typer.Typer()
@app.command()
def main(
operate: ConvertType = typer.Option(
default=ConvertType.XML2JSON, help="operation type"
),
xml: str = typer.Option(default=None, help="where is the xml file or xml dir"),
out: str = typer.Option(default=None, help="write to output instead of stdout"),
):
"""
:param operate: operation type\n
:param xml: where where xml file\n
:param out: write to output instead of stdout\n
"""
if xml is None:
raise typer.BadParameter("Please provide xml file or xml dir")
rpt = CmockaReport(xml, out)
if operate == ConvertType.XML2JSON:
rpt.xml2json()
elif operate == ConvertType.XML2HTML:
rpt.xml2html()
elif operate == ConvertType.MERGEXML:
rpt.mergexml()
else:
pass
if __name__ == "__main__":
app()

View File

@ -0,0 +1,347 @@
"""
Parse a junit report file into a family of objects
"""
from __future__ import unicode_literals
import enum
import os
import uuid
import xml.etree.ElementTree as ET
from .render import HTMLReport
from .textutils import unicode_str
class Outcome(str, enum.Enum):
FAILED = "failure" # the test failed
SKIPPED = "skipped" # the test was skipped
PASSED = "passed" # the test completed successfully
ERROR = "error"
ABSENT = "absent" # the test was known but not run/failed/skipped
def clean_xml_attribute(element, attribute, default=None):
"""
Get an XML attribute value and ensure it is legal in XML
:param element:
:param attribute:
:param default:
:return:
"""
value = element.attrib.get(attribute, default)
if value:
value = value.encode("utf-8", errors="replace").decode(
"utf-8", errors="backslashreplace"
)
value = value.replace("\ufffd", "?") # strip out the unicode replacement char
return value
class ParserError(Exception):
"""
We had a problem parsing a file
"""
def __init__(self, message):
super(ParserError, self).__init__(message)
class ToJunitXmlBase(object):
"""
Base class of all objects that can be serialized to Junit XML
"""
def tojunit(self):
"""
Return an Element matching this object
:return:
"""
raise NotImplementedError()
def make_element(self, xmltag, text=None, attribs=None):
"""
Create an Element and put text and/or attribs into it
:param xmltag: tag name
:param text:
:param attribs: dict of xml attributes
:return:
"""
element = ET.Element(unicode_str(xmltag))
if text is not None:
element.text = unicode_str(text)
if attribs is not None:
for item in attribs:
element.set(unicode_str(item), unicode_str(attribs[item]))
return element
class AnchorBase(object):
"""
Base class that can generate a unique anchor name.
"""
def __init__(self):
self._anchor = None
def id(self):
return self.anchor()
def anchor(self):
"""
Generate a html anchor name
:return:
"""
if not self._anchor:
self._anchor = str(uuid.uuid4())
return self._anchor
class Property(AnchorBase, ToJunitXmlBase):
"""
Test Properties
"""
def __init__(self):
super(Property, self).__init__()
self.name = None
self.value = None
def tojunit(self):
"""
Return the xml element for this property
:return:
"""
prop = self.make_element("property")
prop.set("name", unicode_str(self.name))
prop.set("value", unicode_str(self.value))
return prop
class Case(AnchorBase, ToJunitXmlBase):
"""
Test cases
"""
def __init__(self):
super(Case, self).__init__()
self.msg = None
self.text = None
self.stderr = None
self.stdout = None
self.duration = 0
self.name = None
self.properties = list()
self.outcome = Outcome.PASSED.value
def prefix(self):
if self.outcome == "failure":
return "[F]"
elif self.outcome == "skipped":
return "[S]"
elif self.outcome == "error":
return "[E]"
else:
return ""
class Suite(AnchorBase, ToJunitXmlBase):
"""
Contains test cases (usually only one suite per report)
"""
def __init__(self):
super(Suite, self).__init__()
self.name = None
self.duration = 0
self.cases = list()
self.package = None
self.properties = list()
self.errors = list()
self.stdout = None
self.stderr = None
self.tests_num = 0
self.failures_num = 0
self.errors_num = 0
self.skipped_num = 0
def tojunit(self):
"""
Return an element for this whole suite and all it's cases
:return:
"""
suite = self.make_element("testsuite")
suite.set("name", unicode_str(self.name))
suite.set("time", unicode_str(self.duration))
if self.properties:
props = self.make_element("properties")
for prop in self.properties:
props.append(prop.tojunit())
suite.append(props)
for testcase in self.all():
suite.append(testcase.tojunit())
return suite
def all(self):
"""
Return all testcases
:return:
"""
return self.cases
def failed(self):
"""
Return all the failed testcases
:return:
"""
return [test for test in self.all() if test.failed()]
def skipped(self):
"""
Return all skipped testcases
:return:
"""
return [test for test in self.all() if test.skipped]
def passed(self):
"""
Return all the passing testcases
:return:
"""
return [test for test in self.all() if not test.failed() and not test.skipped()]
class Junit(object):
"""
Parse a single junit xml report
"""
def __init__(self, filename=None, xmlstring=None):
"""
Parse the file
:param filename:
:return:
"""
self.filename = filename
self.tree = None
if filename is not None:
self.tree = ET.parse(filename)
elif xmlstring is not None:
self._read(xmlstring)
else:
raise ValueError("Missing any filename or xmlstring")
self.suites = []
self.process()
def __iter__(self):
return self.suites.__iter__()
def _read(self, xmlstring):
"""
Populate the junit xml document tree from a string
:param xmlstring:
:return:
"""
self.tree = ET.fromstring(xmlstring)
def process(self):
"""
populate the report from the xml
:return:
"""
testrun = False
suites = None
if isinstance(self.tree, ET.ElementTree):
root = self.tree.getroot()
else:
root = self.tree
if root.tag == "testrun":
testrun = True
root = root[0]
if root.tag == "testsuite":
suites = [root]
if root.tag == "testsuites" or testrun:
suites = [x for x in root]
if suites is None:
raise ParserError("could not find test suites in results xml")
suitecount = 0
for suite in suites:
suitecount += 1
cursuite = Suite()
self.suites.append(cursuite)
cursuite.name = clean_xml_attribute(
suite, "name", default="suite-" + str(suitecount)
)
cursuite.package = clean_xml_attribute(suite, "package")
cursuite.duration = float(
suite.attrib.get("time", "0").replace(",", "") or "0"
)
cursuite.tests_num = int(suite.attrib.get("tests", "0"))
cursuite.failures_num = int(suite.attrib.get("failures", "0"))
cursuite.errors_num = int(suite.attrib.get("errors", "0"))
cursuite.skipped_num = int(suite.attrib.get("skipped", "0"))
for element in suite:
if element.tag == "error":
# top level error?
errtag = {
"message": element.attrib.get("message", ""),
"type": element.attrib.get("type", ""),
"text": element.text,
}
cursuite.errors.append(errtag)
if element.tag == "system-out":
cursuite.stdout = element.text
if element.tag == "system-err":
cursuite.stderr = element.text
if element.tag == "properties":
for prop in element:
if prop.tag == "property":
newproperty = Property()
newproperty.name = prop.attrib["name"]
newproperty.value = prop.attrib["value"]
cursuite.properties.append(newproperty)
if element.tag == "testcase":
testcase = element
newcase = Case()
newcase.name = clean_xml_attribute(testcase, "name")
newcase.duration = float(
testcase.attrib.get("time", "0").replace(",", "") or "0"
)
cursuite.cases.append(newcase)
# does this test case have any children?
for child in testcase:
if child.tag in ["skipped", "error", "failure"]:
newcase.text = child.text
if "message" in child.attrib:
newcase.msg = child.attrib["message"]
newcase.outcome = child.tag
elif child.tag == "system-out":
newcase.stdout = child.text
elif child.tag == "system-err":
newcase.stderr = child.text
elif child.tag == "properties":
for property in child:
newproperty = Property()
newproperty.name = property.attrib["name"]
newproperty.value = property.attrib["value"]
newcase.properties.append(newproperty)
def html(self):
"""
Render the test suite as a HTML report with links to errors first.
:return:
"""
doc = HTMLReport()
doc.load(self, os.path.basename(self.filename))
return str(doc)

View File

@ -0,0 +1,31 @@
"""
Render junit reports as HTML
"""
import os
from jinja2 import Environment, FileSystemLoader, select_autoescape
class HTMLReport(object):
def __init__(self):
self.title = ""
self.report = None
def load(self, report, title="JUnit2HTML Report"):
self.report = report
self.title = title
def __iter__(self):
return self.report.__iter__()
def __str__(self) -> str:
current_path = os.path.dirname(os.path.abspath(__file__))
print(current_path)
env = Environment(
loader=FileSystemLoader("{0}/templates".format(current_path)),
autoescape=select_autoescape(["html"]),
)
template = env.get_template("report.html")
print(template)
return template.render(report=self, title=self.title)

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<style type="text/css">
{% include "styles.css" %}
</style>
</head>
<body>
{% block content %}
{% endblock %}
<p class="footer">
Generated by junit2html
</p>
</body>
</html>

View File

@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block content %}
<h1>
Test Report : {{ report.title }}
</h1>
<a id="toc"></a>
<table class="index-table">
<tr>
<td>
<ul class="toc">
{% for suite in report %}
<li>{{suite.name}}
<ul>
{% for test in suite.cases %}
<li><a href="#{{test.anchor()}}">{{test.name}}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</td>
<td class="failure-index">
<ul class="toc">
{% for suite in report %}
{% for test in suite.cases %}
{% if test.outcome != "passed" %}
<li><a href="#{{test.anchor()}}">{{test.prefix()}} {{test.name}}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</td>
</tr>
</table>
{% for suite in report %}
<div class="testsuite">
<h2>Test Suite: {{ suite.name }}</h2>
<a id="{{ suite.anchor() }}"></a>
{% if suite.package %}
<span>Package: {{suite.package}}</span>
{% endif %}
{% if suite.properties %}
<h3>Suite Properties</h3>
<table class="proplist">
{% for prop in suite.properties %}
<tr>
<th>{{prop.name}}</th><td>{{prop.value}}</td>
</tr>
{% endfor %}
</table>
{% endif %}
<h3>summary</h3>
<table class="proplist">
<tr>
<th>time</th><td>{{suite.duration |round(1)}} sec</td>
</tr>
<tr>
<th>tests</th><td>{{suite.tests_num}}</td>
</tr>
<tr>
<th>failures</th><td>{{suite.failures_num}}</td>
</tr>
<tr>
<th>errors</th><td>{{suite.errors_num}}</td>
</tr>
<tr>
<th>skipped</th><td>{{suite.skipped_num}}</td>
</tr>
</table>
<h3>cases</h3>
<div class="testclass">
{% for test in suite.cases %}
<div class="test outcome outcome-{{test.outcome}}">
<a id="{{test.anchor()}}"></a>
<table class="proplist">
<tr><th>name</th><td><b>{{test.name}}</b></td></tr>
<tr><th>outcome</th><td>{{test.outcome}}</td></tr>
<tr><th>time</th><td>{{test.duration|round(1)}} sec</td></tr>
{% if test.msg is not none %}
<tr><td>{{test.msg}}</td></tr>
{% endif %}
</table>
{% if test.text is not none %}
<pre>{{test.text}}</pre>
{% endif %}
{% if test.properties %}
<table class="proplist">
{% for prop in test.properties %}
<tr>
<th>{{prop.name}}</th><td>{{prop.value}}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if test.stdout %}
<div class="stdout"><i>Stdout</i><br>
<pre>{{test.stdout}}</pre>
</div>
{% endif %}
{% if test.stderr %}
<div class="stderr"><i>Stderr</i><br>
<pre>{{test.stderr}}</pre>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% if suite.stdout or suite.stderr %}
<h3>Suite stdout:</h3>
<pre class="stdio">{{suite.stdout}}</pre>
<h3>Suite stderr:</h3>
<pre class="stdio">{{suite.stderr}}</pre>
{% endif %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,196 @@
body {
background-color: white;
padding-bottom: 20em;
margin: 0;
min-height: 15cm;
}
h1, h2, h3, h4, h5, h6, h7 {
font-family: sans-serif;
}
h1 {
background-color: #007acc;
color: white;
padding: 3mm;
margin-top: 0;
margin-bottom: 1mm;
}
.footer {
font-style: italic;
font-size: small;
text-align: right;
padding: 1em;
}
.testsuite {
padding-bottom: 2em;
margin-left: 1em;
}
.proplist {
width: 100%;
margin-bottom: 2em;
border-collapse: collapse;
border: 1px solid lightgrey;
}
.proplist th {
background-color: silver;
width: 5em;
padding: 2px;
padding-right: 1em;
text-align: left;
}
.proplist td {
padding: 2px;
}
.index-table {
width: 90%;
margin-left: 1em;
}
.index-table td {
vertical-align: top;
width: 50%;
}
.failure-index {
}
.toc {
margin-bottom: 2em;
font-family: monospace;
}
.stdio, pre {
min-height: 1em;
background-color: #1e1e1e;
color: silver;
padding: 0.5em;
}
.tdpre {
background-color: #1e1e1e;
}
.test {
margin-left: 0.5cm;
}
.outcome {
border-left: 1em;
padding: 2px;
}
.outcome-passed {
border-left: 1em solid lightgreen;
}
.outcome-skipped {
border-left: 1em solid gold;
}
.outcome-failure {
border-left: 1em solid lightcoral;
}
.outcome-error {
border-left: 1em solid tomato;
}
.stats-table {
}
.stats-table td {
min-width: 4em;
text-align: right;
}
.stats-table .failed {
background-color: lightcoral;
}
.stats-table .passed {
background-color: lightgreen;
}
.matrix-table {
table-layout: fixed;
border-spacing: 0;
width: available;
margin-left: 1em;
}
.matrix-table td {
vertical-align: center;
}
.matrix-table td:last-child {
width: 0;
}
.matrix-table tr:hover {
background-color: yellow;
}
.matrix-axis-name {
white-space: nowrap;
padding-right: 0.5em;
border-left: 1px solid black;
border-top: 1px solid black;
text-align: right;
}
.matrix-axis-line {
border-left: 1px solid black;
width: 0.5em;
}
.matrix-classname {
text-align: left;
width: 100%;
border-top: 2px solid grey;
border-bottom: 1px solid silver;
}
.matrix-casename {
text-align: left;
font-weight: normal;
font-style: italic;
padding-left: 1em;
border-bottom: 1px solid silver;
}
.matrix-result {
display: block;
width: 1em;
text-align: center;
padding: 1mm;
margin: 0;
}
.matrix-result-combined {
white-space: nowrap;
padding-right: 0.2em;
text-align: right;
}
.matrix-result-failed {
background-color: lightcoral;
}
.matrix-result-passed {
background-color: lightgreen;
}
.matrix-result-skipped {
background-color: lightyellow;
}
.matrix-even {
background-color: lightgray;
}

View File

@ -0,0 +1,14 @@
"""
Stringify to unicode
"""
def unicode_str(text):
"""
Convert text to unicode
:param text:
:return:
"""
if isinstance(text, bytes):
return text.decode("utf-8", "strict")
return str(text)