add cmocka tools and fix __WORDSIZE not defined warning
This commit is contained in:
parent
da9903fe57
commit
f9f63dc8e1
@ -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()
|
45
testing/cmocka/0004-cmocka-xml-report.patch
Normal file
45
testing/cmocka/0004-cmocka-xml-report.patch
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
216
testing/cmocka/tools/cmocka_implement.py
Normal file
216
testing/cmocka/tools/cmocka_implement.py
Normal 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()
|
152
testing/cmocka/tools/cmocka_report.py
Normal file
152
testing/cmocka/tools/cmocka_report.py
Normal 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()
|
347
testing/cmocka/tools/junit2htmlreport/parser.py
Normal file
347
testing/cmocka/tools/junit2htmlreport/parser.py
Normal 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)
|
31
testing/cmocka/tools/junit2htmlreport/render.py
Normal file
31
testing/cmocka/tools/junit2htmlreport/render.py
Normal 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)
|
17
testing/cmocka/tools/junit2htmlreport/templates/base.html
Normal file
17
testing/cmocka/tools/junit2htmlreport/templates/base.html
Normal 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>
|
121
testing/cmocka/tools/junit2htmlreport/templates/report.html
Normal file
121
testing/cmocka/tools/junit2htmlreport/templates/report.html
Normal 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 %}
|
196
testing/cmocka/tools/junit2htmlreport/templates/styles.css
Normal file
196
testing/cmocka/tools/junit2htmlreport/templates/styles.css
Normal 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;
|
||||
}
|
14
testing/cmocka/tools/junit2htmlreport/textutils.py
Normal file
14
testing/cmocka/tools/junit2htmlreport/textutils.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user