| #!/usr/bin/python |
| """Parses AKAROS output to detect tests and report on them. |
| Arguments: |
| [0]: Path to file containing AKAROS output. |
| [1]: Path to directory where to save test reports. |
| [2]: IDs of tests suites to look for. |
| """ |
| import markup |
| import re |
| import sys |
| |
| |
| |
| class TestSuite() : |
| """Represents a test suite (collection of test cases) and has the ability of |
| printing itself plus the test cases into XML markup. |
| """ |
| def __init__(self, name, class_name) : |
| """The tests will be reported as belonging to 'name.class_name', so you |
| can represent two levels of hierarchy this way. |
| """ |
| self.name = name |
| self.class_name = class_name |
| self.test_cases = [] |
| self.errors_num = 0 |
| self.failures_num = 0 |
| self.skipped_num = 0 |
| |
| def add_test_case(self, name, status, el_time=None, failure_msg=None) : |
| """Adds a test case to the suite. |
| """ |
| test_case = TestCase(self, name, status, el_time, failure_msg) |
| self.test_cases.append(test_case) |
| |
| if status == 'DISABLED' : |
| self.skipped_num += 1 |
| elif status == 'FAILED' : |
| self.failures_num += 1 |
| |
| def generate_markup(self) : |
| """Generates and returns a string containing the representation of the |
| suite and the testcases in XML XUnit format. |
| |
| Returns: |
| String containing the representation. |
| """ |
| report = markup.page( mode='xml' ) |
| report.init( encoding='UTF-8' ) |
| |
| report.testsuite.open(name='akaros_tests', tests=len(self.test_cases), \ |
| errors=self.errors_num, \ |
| failures=self.failures_num, \ |
| skip=self.skipped_num) |
| for test_case in self.test_cases: |
| test_case.generate_markup(report) |
| report.testsuite.close() |
| |
| return report |
| |
| class TestCase() : |
| """Represents a test case, and has ability to print it into a markup page. |
| """ |
| def __init__(self, suite, name, status, el_time=None, failure_msg=None) : |
| self.suite = suite |
| self.name = name |
| self.status = status |
| if self.status in ['PASSED', 'FAILED'] : |
| self.el_time = el_time |
| if self.status == 'FAILED' : |
| self.failure_msg = failure_msg |
| |
| def generate_markup(self, report) : |
| """Generates XML markup representing the test case, and in XUnit format |
| in the given markup.page file. |
| """ |
| full_name = self.suite.name + '.' + self.suite.class_name |
| |
| if self.status in ['PASSED', 'FAILED'] : |
| report.testcase.open(classname=full_name, name=self.name, \ |
| time=self.el_time) |
| else : |
| report.testcase.open(classname=full_name, name=self.name) |
| |
| if self.status == 'DISABLED' : |
| report.skipped.open(type='DISABLED', message='Disabled') |
| report.skipped.close() |
| elif self.status == 'FAILED' : |
| report.failure.open(type='FAILED', message=self.failure_msg) |
| report.failure.close() |
| |
| report.testcase.close() |
| |
| |
| |
| class TestParser() : |
| """This class is a helper for parsing the output from test suite groups |
| ran inside AKAROS. |
| |
| Tests must be printed on to a file (specified by test_output_path) with the |
| following format: |
| <-- BEGIN_{test_suite_name}_{test_class_name}_TESTS --> |
| (PASSED|FAILED|DISABLED) [{test_case_name}]({test_et}s)? {failure_msg}? |
| (PASSED|FAILED|DISABLED) [{test_case_name}]({test_et}s)? {failure_msg}? |
| ... |
| <-- END_{test_suite_name}_{test_class_name}_TESTS --> |
| |
| For example: |
| <-- BEGIN_KERNEL_PB_TESTS --> |
| PASSED [test_easy_to_pass](1.000s) |
| FAILED [test_will_fail](0.01s) This test should do X and Y. |
| DISABLED [test_useless] |
| ... |
| <-- END_KERNEL_PB_TESTS --> |
| """ |
| |
| def __init__(self, test_output_path, test_suite_name, test_class_name) : |
| self.test_output = open(test_output_path, 'r') |
| self.regex_test_start = \ |
| re.compile('^\s*<--\s*BEGIN_%s_%s_TESTS\s*-->\s*$' \ |
| % (test_suite_name, test_class_name)) |
| self.regex_test_end = \ |
| re.compile('^\s*<--\s*END_%s_%s_TESTS\s*-->\s*$' \ |
| % (test_suite_name, test_class_name)) |
| self.test_suite_name = test_suite_name |
| self.test_class_name = test_class_name |
| |
| # Prepare for reading. |
| self.__advance_to_beginning_of_tests() |
| |
| def __advance_to_beginning_of_tests(self) : |
| beginning_reached = False |
| while not beginning_reached : |
| line = self.test_output.readline() |
| if (re.match(self.regex_test_start, line)) : |
| beginning_reached = True |
| elif (len(line) == 0) : |
| exc_msg = 'Could not find tests for {0}_{1}.' |
| exc_msg = exc_msg.format(self.test_suite_name, \ |
| self.test_class_name) |
| raise Exception(exc_msg) |
| |
| def __extract_test_result(self, line) : |
| regex = r'^\s*([A-Z]+)\s*.*$' |
| matchRes = re.match(regex, line) |
| return matchRes.group(1) |
| |
| def __extract_test_name(self, line) : |
| regex = r'^\s*(?:[A-Z]+)\s*\[([a-zA-Z_-]+)\].*$' |
| matchRes = re.match(regex, line) |
| return matchRes.group(1) |
| |
| def __extract_test_elapsed_time(self, line) : |
| regex= r'^\s*(?:PASSED|FAILED)\s*\[(?:[a-zA-Z_-]+)\]\(([0-9\.]+)s\).*$' |
| matchRes = re.match(regex, line) |
| return matchRes.group(1) |
| |
| def __extract_test_fail_msg(self, line) : |
| regex = r'^\s*FAILED\s*\[(?:[a-zA-Z_-]+)\](?:\(.*?\))\s+(.*)$' |
| matchRes = re.match(regex, line) |
| return matchRes.group(1) |
| |
| def __next_test(self) : |
| """Parses the next test from the test output file. |
| Returns: |
| First, True if there was a next test and we had not reached the end. |
| Second, a String with the name of the test. |
| Third, result of the test (PASSED, FAILED, DISABLED). |
| Fourth, time elapsed in seconds, with 3 decimals. |
| Fifth, message of a failed test. |
| """ |
| # Look for test. |
| line = '' |
| while len(line) < 8 : |
| line = self.test_output.readline() |
| if (len(line) == 0) : # EOF |
| return False, '', '', '' |
| |
| if (re.match(self.regex_test_end, line)) : |
| return False, '', '', 0, '' |
| else : |
| name = self.__extract_test_name(line) |
| res = self.__extract_test_result(line) |
| time = self.__extract_test_elapsed_time(line) \ |
| if res in ['FAILED', 'PASSED'] else None |
| msg = self.__extract_test_fail_msg(line) if res == 'FAILED' \ |
| else None |
| |
| return True, name, res, time, msg |
| |
| def __cleanup(self) : |
| self.test_output.close() |
| |
| def parse_test_suite(self) : |
| test_suite = TestSuite(self.test_suite_name, self.test_class_name) |
| |
| end_not_reached = True |
| while end_not_reached : |
| end_not_reached, test_name, test_res, test_et, fail_msg \ |
| = self.__next_test() |
| if end_not_reached : |
| test_suite.add_test_case(test_name, test_res, test_et, fail_msg) |
| |
| self.__cleanup() |
| |
| return test_suite |
| |
| |
| def extract_tests(path): |
| regex = r'^\s*<--\s*BEGIN_(.+_.+)_TESTS\s*-->\s*$' |
| f = open(path, 'r') |
| tests = [] |
| for line in f: |
| matchRes = re.match(regex, line) |
| if matchRes: |
| tests.append(matchRes.group(1)) |
| return tests |
| |
| def save_report(dir, filename, report) : |
| filepath = dir + '/' + filename + '_TESTS.xml' |
| report_file = open(filepath, 'w+') |
| report_file.write(report) |
| report_file.flush() |
| report_file.close() |
| |
| |
| def main() : |
| akaros_output_file_path = sys.argv[1] |
| test_output_dir = sys.argv[2] |
| tests = extract_tests(akaros_output_file_path) |
| |
| # Parse, process, and save the test results |
| for test in tests : |
| suite_name, class_name = test.split('_') |
| test_suite = TestParser(akaros_output_file_path, \ |
| suite_name, class_name).parse_test_suite() |
| test_report_str = test_suite.generate_markup().__str__() |
| save_report(test_output_dir, test, test_report_str) |
| |
| main() |