diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0f44a924ab22d30101823d3b66f88e5b6b4700ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.xlsx +__pycache__/ +.vscode/ \ No newline at end of file diff --git a/parseTestResults.py b/parseTestResults.py deleted file mode 100644 index 9aefe0be7f9534a125cfc173b739fc640707fa59..0000000000000000000000000000000000000000 --- a/parseTestResults.py +++ /dev/null @@ -1,88 +0,0 @@ -from bs4 import BeautifulSoup -from sys import argv -from testEntry import TestEntry -from writeExcel import ExcelWriter -from sys import exit -import re - -usage_str = """ -===================================== -Robot Test Reporter written in Python -===================================== - -Usage - - python parseTestResults.py output.xml - - where output.xml is the xml file generated by robot - - -The command outputs to a new xlslx file if it does not exist, or -appends to an existing one. -""" - -class TestOutputParser: - def __init__(self, fname): - self.testEntries = [] - self.load_file(fname) - - def load_file(self, fname): - self.contents = "" - - with open(fname, "r", encoding="utf8") as f: - self.contents = f.read() - - if self.contents == "": - print("Empty file {}".format(fname)) - exit(-1) - - def run_parser(self): - soup = BeautifulSoup(self.contents, "lxml") - - # Suite information - suite = soup.find("suite") - path = suite["source"] - # TODO This might be an issue later on. In Unix-style paths the separator is a forward slash - parts = path.split("\\") - # Extract info for test entries - self.api = parts[len(parts) - 2] - self.robotFile = parts[len(parts) - 1] - - # Tests - tests = soup.find_all("test") - for test in tests: - self.testEntries.append(self.createTestEntry(test)) - - # Write tests - ew = ExcelWriter() - for entry in self.testEntries: - ew.writeTestEntry(entry) - ew.save() - - def createTestEntry(self, xmlObj): - """ - Takes the xml entry corresponding to the test from the output file, - and returns a TestEntry object with the relevant information extracted. - """ - # retrieve ID and name - idRaw = xmlObj.find("doc", recursive=False).contents - mg = re.search(r"Test ID: ([0-9\.]*)$", idRaw[0].string, re.MULTILINE) - testId = mg.group(1) - name = xmlObj["name"] - - #retrieve status and error message (if FAIL) - statusObj = xmlObj.find("status", recursive=False) - cts = statusObj.contents - errorMsg = cts[0] if len(cts) > 0 else "" - - result = statusObj["status"] - return TestEntry(testId, name, result, errorMsg, self.api, self.robotFile) - -def display_usage(): - print(usage_str) - -if __name__ == "__main__": - if len(argv) < 2: - display_usage() - exit() - TestOutputParser(argv[1]).run_parser() \ No newline at end of file diff --git a/parse_test_results.py b/parse_test_results.py index 0682eb0871ecb6029638a3b0fcd2e96ef3050e36..409f36333f65807587f6e611c13288961f6fadd1 100644 --- a/parse_test_results.py +++ b/parse_test_results.py @@ -1,52 +1,55 @@ -from bs4 import BeautifulSoup -from sys import argv -from testEntry import TestEntry -from writeExcel import ExcelWriter +""" +Tool entry point +""" + +import sys import re import argparse - -usage_str = """ -===================================== -Robot Test Reporter written in Python -===================================== - -Usage - - python parseTestResults.py output.xml - - where output.xml is the xml file generated by robot +from bs4 import BeautifulSoup +from test_entry import TestEntry +from write_excel import ExcelWriter -The command outputs to a new xlslx file if it does not exist, or -appends to an existing one. -""" class TestOutputParser: - def __init__(self, fname): + """ + Parser taking a file name (of the XML output from robot), + and extracts relevant information, eventually creating a list + of TestEntry objects + """ + def __init__(self, input_file, output_file): self.test_entries = [] - self.load_file(fname) + self.load_file(input_file) + self.api = "" + self.output_file = output_file + self.robot_file_contents = "" def load_file(self, fname): - self.contents = "" + """ + Load xml file + """ - with open(fname, "r", encoding="utf8") as f: - self.contents = f.read() + with open(fname, "r", encoding="utf8") as robot_file: + self.robot_file_contents = robot_file.read() - if self.contents == "": + if self.robot_file_contents == "": print("Empty file {}".format(fname)) - exit(-1) + sys.exit(-1) def run_parser(self): - soup = BeautifulSoup(self.contents, "lxml") + """ + Run parser, extracting all info to create the test entries + """ + soup = BeautifulSoup(self.robot_file_contents, "lxml") # Suite information suite = soup.find("suite") path = suite["source"] - # TODO This might be an issue later on. In Unix-style paths the separator is a forward slash + parts = path.split("\\") # Extract info for test entries self.api = parts[len(parts) - 2] - self.robot_file = parts[len(parts) - 1] + self.robot_file_contents = parts[len(parts) - 1] # Tests tests = soup.find_all("test") @@ -54,10 +57,10 @@ class TestOutputParser: self.test_entries.append(self.create_test_entry(test)) # Write tests - ew = ExcelWriter() + excel_writer = ExcelWriter(self.output_file) for entry in self.test_entries: - ew.write_test_entry(entry) - ew.save() + excel_writer.write_test_entry(entry) + excel_writer.save() def create_test_entry(self, xml_obj): """ @@ -66,44 +69,28 @@ class TestOutputParser: """ # retrieve ID and name id_raw = xml_obj.find("doc", recursive=False).contents - mg = re.search(r"Test ID: ([0-9\.]*)$", id_raw[0].string, re.MULTILINE) - test_id = mg.group(1) + match_group = re.search(r"Test ID: ([0-9\.]*)$", id_raw[0].string, re.MULTILINE) + test_id = match_group.group(1) name = xml_obj["name"] #retrieve status and error message (if FAIL) - statusObj = xml_obj.find("status", recursive=False) - cts = statusObj.contents + status_obj = xml_obj.find("status", recursive=False) + cts = status_obj.contents error_msg = cts[0] if len(cts) > 0 else "" - result = statusObj["status"] - return TestEntry(test_id, name, result, error_msg, self.api, self.robot_file) - -def display_usage(): - print(usage_str) + result = status_obj["status"] + return TestEntry(test_id, name, (result, error_msg), (self.api, self.robot_file_contents)) if __name__ == "__main__": - usage_str = """ -===================================== -Robot Test Reporter written in Python -===================================== - -Usage - - python parseTestResults.py output.xml - - where output.xml is the xml file generated by robot - - -The command outputs to a new xlslx file if it does not exist, or -appends to an existing one. -""" - parser = argparse.ArgumentParser(description='Process some integers.') - parser.add_argument('integers', metavar='N', type=int, nargs='+', - help='an integer for the accumulator') - parser.add_argument('-o', dest='fname', action='store_const', - default='testResults.xlsx', + parser = argparse.ArgumentParser(description='Robot Test Reporter written in Python.\n' + 'Produces an xlsx file from a robot XML ' + 'output file.\n' + 'The command outputs to a new xlslx file if it ' + 'does not exist, or appends to an existing one.') + parser.add_argument('input_file', help='XML file generated by Robot') + parser.add_argument('-o', dest='output_file', default='testResults.xlsx', help='output file name (default: testResults.xlsx)') args = parser.parse_args() - TestOutputParser(argv[1]).run_parser() + TestOutputParser(args.input_file, args.output_file).run_parser() diff --git a/testEntry.py b/testEntry.py deleted file mode 100644 index 6afbaefab6406b92d79fcadf88154d8b96fe0427..0000000000000000000000000000000000000000 --- a/testEntry.py +++ /dev/null @@ -1,21 +0,0 @@ -class TestEntry: - def __init__(self, id, name, result, errorMsg, api, robotFile): - self.id = id - self.name = name - self.result = result - self.errorMsg = errorMsg - self.api = api - self.robotFile = robotFile - - def __str__(self): - baseStr = "[{}] {}:{}".format(self.id, self.name, self.result) - if self.errorMsg != "": - return "{}: {} ({}/{})".format(baseStr, self.errorMsg, self.api, self.robotFile) - else: - return baseStr - - def __repr__(self): - return "{}: {}".format(self.name, self.result) - - def asList(self): - return [self.id, self.name, self.result, self.errorMsg, self.api, self.robotFile] \ No newline at end of file diff --git a/test_entry.py b/test_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..4980ab8b9c1b9e2bedc92cea53e76c7fed564575 --- /dev/null +++ b/test_entry.py @@ -0,0 +1,38 @@ +""" +Contains utility class `TestEntry` +""" + +class TestEntry: + """ + Models a single entry (row) in the resulting xlsx file. + + Contains all the relevant information about a single test: + id: Test ID + name: Test Name + result: PASS/FAIL + error_message: In case of FAIL, contains reason + robot_file: Robot file containing the test + """ + def __init__(self, test_id, name, status, test_info): + self.test_id = test_id + self.name = name + self.result = status[0] + self.error_message = status[1] + self.api = test_info[0] + self.robot_file = test_info[1] + + def __str__(self): + base_str = "[{}] {}:{}".format(self.test_id, self.name, self.result) + if self.error_message != "": + return "{}: {} ({}/{})".format(base_str, self.error_message, self.api, self.robot_file) + + return base_str + + def __repr__(self): + return "{}: {}".format(self.name, self.result) + + def as_list(self): + """ + Construct a list-representation of the entry + """ + return [self.test_id, self.name, self.result, self.error_message, self.api, self.robot_file] diff --git a/writeExcel.py b/writeExcel.py deleted file mode 100644 index 76192e1d82ed66e0e4e13fa89f5e120d9ec047c5..0000000000000000000000000000000000000000 --- a/writeExcel.py +++ /dev/null @@ -1,85 +0,0 @@ -from openpyxl import Workbook, load_workbook -from openpyxl.utils import get_column_letter -from openpyxl.styles import Font -from openpyxl.styles.fills import Stop, PatternFill - -fname = 'testResults.xlsx' - -class ExcelWriter: - """ - Utility class which writes `TestEntry` objects to an excel file. - Creates file if it does not exist. - """ - PASS_COL = "00FF00" - FAIL_COL = "FF0000" - def __init__(self): - self.wb = getWorkbook() - self.ws = self.wb.active - - def get_entry_with_id(self, id): - """ - When inserting an entry, it might be a test being re-run. In that case, - the row containing that test needs to be updated, as opposed to appending - the entry to the end of the file. - This method finds that row, returning the row number, - or -1 in the case where that test is not in the report yet. - """ - for cell in self.ws["A"]: - if cell.value is None: - return -1 - elif cell.value == id: - print("MATCH with id {} at row {}".format(id, cell.row)) - return cell.row - return -1 - - def get_last_row(self): - for cell in self.ws["A"]: - if cell.value is None: - return cell.row - return cell.row + 1 - - def writeTestEntry(self, test_entry): - existing_entry_row = self.get_entry_with_id(test_entry.id) - last_row = self.get_last_row() - # Use the above two values to pick a row - entry_row = existing_entry_row if existing_entry_row != -1 else last_row - - # Pick a cell color based on test outcome - cell_col = ExcelWriter.PASS_COL if test_entry.result == "PASS" else ExcelWriter.FAIL_COL - - # Test entry as a list - entry_vals = test_entry.asList() - - for col, cell_vale in zip(self.ws.iter_cols(min_row=entry_row, max_col=len(entry_vals), max_row=entry_row), entry_vals): - for cell in col: - cell.value = cell_vale - cell.fill = PatternFill("solid", fgColor=cell_col)# cellCol - - - def save(self): - self.wb.save(filename = fname) - -def initWorkbook(wb): - """ - Writes column headers to ws - """ - headers = [("Test ID", 10), ("Test name", 80), ("Result", 6), ("Error Message", 100), ("NFV API", 25), ("Robot Test File", 25)] - headerFont = Font(bold=True) - ws = wb.active - for col, header in zip(ws.iter_cols(min_row=1, max_col=len(headers), max_row=1), headers): - for cell in col: - headerName = header[0] - colsize = header[1] - ws.column_dimensions[get_column_letter(cell.column)].width = colsize - cell.value = headerName - cell.font = headerFont - - -def getWorkbook(): - try: - wb = load_workbook(filename = fname) - return wb - except FileNotFoundError: - wb = Workbook() - initWorkbook(wb) - return wb \ No newline at end of file diff --git a/write_excel.py b/write_excel.py new file mode 100644 index 0000000000000000000000000000000000000000..aea5429ba9f0177df8c8fbeb46418fb838b46999 --- /dev/null +++ b/write_excel.py @@ -0,0 +1,103 @@ +""" +Contains utility class for writing to xlsx +""" + +from openpyxl import Workbook, load_workbook +from openpyxl.utils import get_column_letter +from openpyxl.styles import Font +from openpyxl.styles.fills import PatternFill + +class ExcelWriter: + """ + Utility class which writes `TestEntry` objects to an excel file. + Creates file if it does not exist. + """ + PASS_COL = "00FF00" + FAIL_COL = "FF0000" + def __init__(self, output_file): + self.work_book = self.get_workbook() + self.work_sheet = self.work_book.active + self.output_file = output_file + + def get_entry_with_id(self, test_id): + """ + When inserting an entry, it might be a test being re-run. In that case, + the row containing that test needs to be updated, as opposed to appending + the entry to the end of the file. + This method finds that row, returning the row number, + or -1 in the case where that test is not in the report yet. + """ + for cell in self.work_sheet["A"]: + if cell.value is None: + return -1 + if cell.value == test_id: + print("MATCH with id {} at row {}".format(test_id, cell.row)) + return cell.row + return -1 + + def get_last_row(self): + """ + return index of first empty row + """ + for cell in self.work_sheet["A"]: + if cell.value is None: + return cell.row + return 1 + + def write_test_entry(self, test_entry): + """ + Write a test entry to the work_sheet + """ + existing_entry_row = self.get_entry_with_id(test_entry.id) + last_row = self.get_last_row() + # Use the above two values to pick a row + entry_row = existing_entry_row if existing_entry_row != -1 else last_row + + # Pick a cell color based on test outcome + cell_col = ExcelWriter.PASS_COL if test_entry.result == "PASS" else ExcelWriter.FAIL_COL + + # Test entry as a list + entry_vals = test_entry.as_list() + + for col, cell_vale in zip(self.work_sheet.iter_cols(min_row=entry_row, + max_col=len(entry_vals), + max_row=entry_row), entry_vals): + for cell in col: + cell.value = cell_vale + cell.fill = PatternFill("solid", fgColor=cell_col)# cellCol + + def get_workbook(self): + """ + Returns workbook at output_file/. + Creates and initialises it (adds headers) if it doesn't exist yet. + """ + try: + work_book = load_workbook(filename=self.output_file) + return work_book + except FileNotFoundError: + work_book = Workbook() + init_workbook(work_book) + return work_book + + def save(self): + """ + Save workbook to disk. + """ + self.work_book.save(filename=self.output_file) + +def init_workbook(work_book): + """ + Writes column headers to ws. + """ + headers = [("Test ID", 10), ("Test name", 80), ("Result", 6), ("Error Message", 100), + ("NFV API", 25), ("Robot Test File", 25)] + header_font = Font(bold=True) + work_sheet = work_book.active + for col, header in zip(work_sheet.iter_cols(min_row=1, max_col=len(headers), + max_row=1), headers): + for cell in col: + header_name = header[0] + colsize = header[1] + work_sheet.column_dimensions[get_column_letter(cell.column)].width = colsize + cell.value = header_name + cell.font = header_font