|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import copy |
| 4 | +import json |
| 5 | +import openpyxl |
| 6 | +from openpyxl.styles import Color, PatternFill |
| 7 | +from openpyxl.styles.differential import DifferentialStyle |
| 8 | +from openpyxl.formatting.rule import Rule, FormulaRule |
| 9 | +import os |
| 10 | +import re |
| 11 | +import subprocess |
| 12 | +import sys |
| 13 | + |
| 14 | +def eprint(*args, **kwargs): |
| 15 | + print(*args, file=sys.stderr, **kwargs) |
| 16 | + |
| 17 | + |
| 18 | +def parse_function(function): |
| 19 | + eprint(function) |
| 20 | + # Remove whitespace and trailing semicolon |
| 21 | + function = function.strip().strip(';') |
| 22 | + # For parsing make sure there is no space before the parenthesis |
| 23 | + function = re.sub(r' *\(', '(', function) |
| 24 | + # Replace (void) notation with empty parentheses |
| 25 | + function = re.sub(r'\( *void *\)', '()', function) |
| 26 | + # Parse return type, function name and parameter string |
| 27 | + match = re.match(r'^(.*[\* ])(\w+)\((.*)\)$', function) |
| 28 | + if not match: |
| 29 | + raise Exception(f'Function did not match regex: {function}') |
| 30 | + func_data = {} |
| 31 | + func_data['name'] = match.group(2).strip() |
| 32 | + # Parse modifiers and return type |
| 33 | + mod_ret_str = match.group(1).strip() |
| 34 | + mod_ret = mod_ret_str.split(' ') |
| 35 | + mod_ret = [mr.strip() for mr in mod_ret if mr] |
| 36 | + # Fix pointer return type which could be split up |
| 37 | + if len(mod_ret) >= 2 and mod_ret[-1] == '*': |
| 38 | + mod_ret[-2] += ' *' |
| 39 | + del mod_ret[-1] |
| 40 | + func_data['return_type'] = mod_ret[-1] |
| 41 | + func_data['attributes'] = mod_ret[:-1] |
| 42 | + # Parse parameters |
| 43 | + func_data['params'] = {} |
| 44 | + if match.group(3).strip() != '': |
| 45 | + params_str = [param_str.strip() for param_str in match.group(3).split(',')] |
| 46 | + for i, param_str in enumerate(params_str): |
| 47 | + eprint(param_str) |
| 48 | + pmatch = re.fullmatch(r'(.*[\* ])([\w\[\]]+)', param_str) |
| 49 | + if pmatch: |
| 50 | + func_data['params'][pmatch.group(2).strip()] = pmatch.group(1).strip() |
| 51 | + else: |
| 52 | + ptmatch = re.fullmatch(r'([\w]+(?: *)?(?:\**)?)', param_str) |
| 53 | + if ptmatch: |
| 54 | + func_data['params'][f'param{i}'] = ptmatch.group(1).strip() |
| 55 | + else: |
| 56 | + raise Exception(f'Parameter in function "{function}" did not match regex: {param_str}') |
| 57 | + return func_data |
| 58 | + |
| 59 | +# Builds the function declaration for a function |
| 60 | +def get_function_declaration(func): |
| 61 | + decl_params = ', '.join([f'{p_type} {p_name}' for p_name, p_type in func['params'].items()]) |
| 62 | + return f'{" ".join(func["attributes"])} {func["return_type"]} {func["name"]}({decl_params})'.strip() |
| 63 | + |
| 64 | +# Extracts functions from source files by calling the 'get_source_context.py' script |
| 65 | +def get_source_context(cmd, source_files): |
| 66 | + try: |
| 67 | + return subprocess.run([sys.executable, 'get_source_context.py', cmd, *source_files], cwd='..', check=True, stdout=subprocess.PIPE).stdout.decode().strip().split('\n') |
| 68 | + except subprocess.CalledProcessError: |
| 69 | + eprint('Failed to extract functions from source') |
| 70 | + sys.exit(1) |
| 71 | + |
| 72 | +# Searches the source files for "#define func_name other_func_name" statements |
| 73 | +# and returns them as a dict |
| 74 | +RE_DEFINE_LINE = r'^\s*#define\s+(\w+)\s+(\w+)\s*$' |
| 75 | +def find_redefinitions(source_files): |
| 76 | + redefinitions = {} |
| 77 | + for source_file in source_files: |
| 78 | + with open(source_file, 'r') as f: |
| 79 | + for line in f: |
| 80 | + match = re.fullmatch(RE_DEFINE_LINE, line) |
| 81 | + if match: |
| 82 | + redefinitions[match.group(1)] = match.group(2) |
| 83 | + return redefinitions |
| 84 | + |
| 85 | +# Writes functions to an excel sheet |
| 86 | +def write_functions_to_excel(filename, funcs, prefill_params=False): |
| 87 | + # Create an excel document |
| 88 | + wb = openpyxl.Workbook() |
| 89 | + sheet_funcs = wb.create_sheet('functions') |
| 90 | + sheet_funcs.title = 'functions' |
| 91 | + sheet_funcs.column_dimensions['A'].width = 120 |
| 92 | + for i in range(6): |
| 93 | + sheet_funcs.column_dimensions[chr(ord('B')+i)].width = 25 |
| 94 | + sheet_snips = wb.create_sheet('snippets') |
| 95 | + sheet_snips.title = 'snippets' |
| 96 | + wb.remove(wb['Sheet']) |
| 97 | + # Add highlighting for void/non-void functions |
| 98 | + fill_green = PatternFill(start_color='99ff99', fill_type='solid') |
| 99 | + fill_orange = PatternFill(start_color='ffd699', fill_type='solid') |
| 100 | + #dxf_green = DifferentialStyle(fill=fill_green) |
| 101 | + #dxf_orange = DifferentialStyle(fill=fill_orange) |
| 102 | + #rule_void = Rule(type='containsText', operator='containsText', text='()', dxf=dxf_green) |
| 103 | + #rule_void.formula = ['NOT(ISERROR(SEARCH("()",A1)))'] |
| 104 | + #rule_params = Rule(type='notContainsText', operator='notContains', text='()', dxf=dxf_orange) |
| 105 | + #rule_params.formula = ['ISERROR(SEARCH("()",A1))'] |
| 106 | + #ws.conditional_formatting.add('A1:A1000', rule_void) |
| 107 | + |
| 108 | + for idx in range(len(funcs)): |
| 109 | + # Write function |
| 110 | + func = funcs[idx] |
| 111 | + sheet_funcs.cell(row=1+idx, column=1).value = get_function_declaration(func) |
| 112 | + sheet_funcs.cell(row=1+idx, column=1).fill = fill_green if len(func['params']) == 0 else fill_orange |
| 113 | + # Write function parameter names |
| 114 | + if prefill_params: |
| 115 | + p_col = 2 |
| 116 | + for p_name in func['params']: |
| 117 | + sheet_funcs.cell(row=1+idx, column=p_col).value = f'{p_name}: ' |
| 118 | + p_col += 1 |
| 119 | + |
| 120 | + # Save document |
| 121 | + wb.save(filename) |
| 122 | + |
| 123 | + |
| 124 | +def main(): |
| 125 | + if len(sys.argv) < 2: |
| 126 | + eprint(f'Usage: {sys.argv[0]} <source files>... <mock header files>...') |
| 127 | + sys.exit(1) |
| 128 | + |
| 129 | + # Separate arguments into implementation source files and mock header files |
| 130 | + source_files = [] |
| 131 | + header_files = [] |
| 132 | + for file_arg in sys.argv[1:]: |
| 133 | + if not os.path.isfile(file_arg): |
| 134 | + eprint(f'{file_arg} not found') |
| 135 | + sys.exit(1) |
| 136 | + _, ext = os.path.splitext(file_arg) |
| 137 | + if ext.startswith('.c'): |
| 138 | + source_files.append(file_arg) |
| 139 | + elif ext.startswith('.h'): |
| 140 | + header_files.append(file_arg) |
| 141 | + else: |
| 142 | + eprint(f'Unknown file type: {file_arg}') |
| 143 | + sys.exit(1) |
| 144 | + |
| 145 | + # Extract all data we need from the provided files, that is: |
| 146 | + # - All declarations from header files that contain mock functions (and possibly other ones) |
| 147 | + # - All function calls inside the source files. We'll use this to filter the mock declarations. |
| 148 | + # - All function definitions (their declarations) in the source files to filter them out of the mock declarations |
| 149 | + eprint('Parsing mock declarations...') |
| 150 | + mock_decls_str = get_source_context('find_func_decls', header_files) |
| 151 | + mock_decls = [parse_function(func) for func in mock_decls_str] |
| 152 | + # Remove the 'extern' attribute |
| 153 | + for mock_decl in mock_decls: |
| 154 | + if 'extern' in mock_decl['attributes']: |
| 155 | + mock_decl['attributes'].remove('extern') |
| 156 | + eprint('Parsing function definitions...') |
| 157 | + func_calls_str = get_source_context('find_func_calls', source_files) |
| 158 | + func_defs_str = get_source_context('find_func_defs', source_files) |
| 159 | + func_defs = [parse_function(func) for func in func_defs_str] |
| 160 | + # Also search preprocessor definitions (we're looking for redefined function names) |
| 161 | + redefs = find_redefinitions(header_files) |
| 162 | + |
| 163 | + # Deduplicate mocks, we just take the shorter declaration for now |
| 164 | + mock_decls_dict = {} |
| 165 | + for mock_decl in mock_decls: |
| 166 | + if mock_decl['name'] in mock_decls_dict: |
| 167 | + decl_str_1 = get_function_declaration(mock_decls_dict[mock_decl['name']]) |
| 168 | + decl_str_2 = get_function_declaration(mock_decl) |
| 169 | + if decl_str_1 == decl_str_2: |
| 170 | + pass |
| 171 | + elif len(decl_str_1) <= len(decl_str_2): |
| 172 | + eprint(f'Mock conflict: -> {decl_str_1}') |
| 173 | + eprint(f'Mock conflict: {decl_str_2}') |
| 174 | + else: |
| 175 | + eprint(f'Mock conflict: {decl_str_1}') |
| 176 | + eprint(f'Mock conflict: -> {decl_str_2}') |
| 177 | + mock_decls_dict[mock_decl['name']] = mock_decl |
| 178 | + else: |
| 179 | + mock_decls_dict[mock_decl['name']] = mock_decl |
| 180 | + mock_decls = list(mock_decls_dict.values()) |
| 181 | + |
| 182 | + # Copy mock declarations when additional new names for them have been defined |
| 183 | + for new_name, old_name in redefs.items(): |
| 184 | + if old_name in mock_decls_dict and new_name not in mock_decls_dict: |
| 185 | + mock_decl = copy.deepcopy(mock_decls_dict[old_name]) |
| 186 | + mock_decl['name'] = new_name |
| 187 | + mock_decls.append(mock_decl) |
| 188 | + |
| 189 | + # Filter the mock declarations to keep called function only and remove functions we have implementations for |
| 190 | + func_defs_names = [func_def['name'] for func_def in func_defs] |
| 191 | + mock_decls = [mock_decl for mock_decl in mock_decls if mock_decl['name'] in func_calls_str and mock_decl['name'] not in func_defs_names] |
| 192 | + |
| 193 | + # Filter the function implementations to include only public functions |
| 194 | + # I.e. remove static functions and functions starting with <module>__ (TODO: Conti specific) |
| 195 | + func_defs = [func_def for func_def in func_defs if 'static' not in func_def['attributes'] and not re.match(r'[a-zA-Z0-9]+__', func_def['name'])] |
| 196 | + |
| 197 | + # Sort the lists first by function name, then group function with/without parameters |
| 198 | + mock_decls.sort(key=lambda x: x['name']) |
| 199 | + mock_decls.sort(key=lambda x: int(len(x['params']) == 0)) |
| 200 | + func_defs.sort(key=lambda x: x['name']) |
| 201 | + func_defs.sort(key=lambda x: int(len(x['params']) == 0)) |
| 202 | + |
| 203 | + # Write excel files |
| 204 | + write_functions_to_excel('testgen_mocks.xlsx', mock_decls) |
| 205 | + write_functions_to_excel('testgen_functions.xlsx', func_defs, prefill_params=True) |
| 206 | + |
| 207 | + |
| 208 | +if __name__ == '__main__': |
| 209 | + main() |
0 commit comments