mirror of
				https://github.com/catchorg/Catch2.git
				synced 2025-10-31 12:17:11 +01:00 
			
		
		
		
	Add script checking convenience header correctness
This commit is contained in:
		
							
								
								
									
										153
									
								
								tools/scripts/checkConvenienceHeaders.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								tools/scripts/checkConvenienceHeaders.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| """ | ||||
| Checks that all of the "catch_foo_all.hpp" headers include all subheaders. | ||||
|  | ||||
| The logic is simple: given a folder, e.g. `catch2/matchers`, then the | ||||
| ccorresponding header is called `catch_matchers_all.hpp` and contains | ||||
| * all headers in `catch2/matchers`, | ||||
| * all headers in `catch2/matchers/{internal, detail}`, | ||||
| * all convenience catch_matchers_*_all.hpp headers from any non-internal subfolders | ||||
|  | ||||
| The top level header is called `catch_all.hpp`. | ||||
| """ | ||||
|  | ||||
| internal_dirs = ['detail', 'internal'] | ||||
| excluded_dirs = ['external'] | ||||
|  | ||||
| from scriptCommon import catchPath | ||||
| from glob import glob | ||||
| from pprint import pprint | ||||
| import os | ||||
| import re | ||||
|  | ||||
| def normalized_path(path): | ||||
|     """Replaces \ in paths on Windows with /""" | ||||
|     return path.replace('\\', '/') | ||||
|  | ||||
| def normalized_paths(paths): | ||||
|     """Replaces \ with / in every path""" | ||||
|     return [normalized_path(path) for path in paths] | ||||
|  | ||||
| source_path = catchPath + '/src/catch2' | ||||
| source_path = normalized_path(source_path) | ||||
| include_parser = re.compile(r'#include <(catch2/.+\.hpp)>') | ||||
|  | ||||
| errors_found = False | ||||
|  | ||||
| def headers_in_folder(folder): | ||||
|     return glob(folder + '/*.hpp') | ||||
|  | ||||
| def folders_in_folder(folder): | ||||
|     return [x for x in os.scandir(folder) if x.is_dir()] | ||||
|  | ||||
| def collated_includes(folder): | ||||
|     base = headers_in_folder(folder) | ||||
|     for subfolder in folders_in_folder(folder): | ||||
|         if folder in excluded_dirs: | ||||
|             continue | ||||
|         if subfolder.name in internal_dirs: | ||||
|             base.extend(headers_in_folder(subfolder.path)) | ||||
|         else: | ||||
|             base.append(subfolder.path + '/catch_{}_all.hpp'.format(subfolder.name)) | ||||
|     return normalized_paths(sorted(base)) | ||||
|  | ||||
| def includes_from_file(header): | ||||
|     includes = [] | ||||
|     with open(header, 'r', encoding = 'utf-8') as file: | ||||
|         for line in file: | ||||
|             if not line.startswith('#include'): | ||||
|                 continue | ||||
|             match = include_parser.match(line) | ||||
|             if match: | ||||
|                 includes.append(match.group(1)) | ||||
|     return normalized_paths(includes) | ||||
|  | ||||
| def normalize_includes(includes): | ||||
|     """Returns """ | ||||
|     return [include[len(catchPath)+5:] for include in includes] | ||||
|  | ||||
| def get_duplicates(xs): | ||||
|     seen = set() | ||||
|     duplicated = [] | ||||
|     for x in xs: | ||||
|         if x in seen: | ||||
|             duplicated.append(x) | ||||
|         seen.add(x) | ||||
|     return duplicated | ||||
|  | ||||
| def verify_convenience_header(folder): | ||||
|     """ | ||||
|     Performs the actual checking of convenience header for specific folder. | ||||
|     Checks that | ||||
|     1) The header even exists | ||||
|     2) That all includes in the header are sorted | ||||
|     3) That there are no duplicated includes | ||||
|     4) That all includes that should be in the header are actually present in the header | ||||
|     5) That there are no superfluous includes that should not be in the header | ||||
|     """ | ||||
|     global errors_found | ||||
|  | ||||
|     path = normalized_path(folder.path) | ||||
|  | ||||
|     assert path.startswith(source_path), '{} does not start with {}'.format(path, source_path) | ||||
|     stripped_path = path[len(source_path) + 1:] | ||||
|     path_pieces = stripped_path.split('/') | ||||
|  | ||||
|     if path == source_path: | ||||
|         header_name = 'catch_all.hpp' | ||||
|     else: | ||||
|         header_name = 'catch_{}_all.hpp'.format('_'.join(path_pieces)) | ||||
|          | ||||
|     # 1) Does it exist? | ||||
|     full_path = path + '/' + header_name | ||||
|     if not os.path.isfile(full_path): | ||||
|         errors_found = True | ||||
|         print('Missing convenience header: {}'.format(full_path)) | ||||
|         return | ||||
|     file_incs = includes_from_file(path + '/' + header_name) | ||||
|     # 2) Are the includes are sorted? | ||||
|     if sorted(file_incs) != file_incs: | ||||
|         errors_found = True | ||||
|         print("'{}': Includes are not in sorted order!".format(header_name)) | ||||
|  | ||||
|     # 3) Are there no duplicates? | ||||
|     duplicated = get_duplicates(file_incs) | ||||
|     for duplicate in duplicated: | ||||
|         errors_found = True | ||||
|         print("'{}': Duplicated include: '{}'".format(header_name, duplicate)) | ||||
|      | ||||
|     target_includes = normalize_includes(collated_includes(path)) | ||||
|     # Avoid requiring the convenience header to include itself | ||||
|     target_includes = [x for x in target_includes if header_name not in x] | ||||
|     # 4) Are all required headers present? | ||||
|     file_incs_set = set(file_incs) | ||||
|     for include in target_includes: | ||||
|         if include not in file_incs_set: | ||||
|             errors_found = True | ||||
|             print("'{}': missing include '{}'".format(header_name, include)) | ||||
|  | ||||
|     # 5) Are there any superfluous headers? | ||||
|     desired_set = set(target_includes) | ||||
|     for include in file_incs: | ||||
|         if include not in desired_set: | ||||
|             errors_found = True | ||||
|             print("'{}': superfluous include '{}'".format(header_name, include))         | ||||
|         | ||||
|      | ||||
|  | ||||
| def walk_source_folders(current): | ||||
|     verify_convenience_header(current) | ||||
|     for folder in folders_in_folder(current.path): | ||||
|         fname = folder.name | ||||
|         if fname not in internal_dirs and fname not in excluded_dirs: | ||||
|             walk_source_folders(folder) | ||||
|  | ||||
| # This is an ugly hack because we cannot instantiate DirEntry manually | ||||
| base_dir = [x for x in os.scandir(catchPath + '/src') if x.name == 'catch2'] | ||||
| walk_source_folders(base_dir[0]) | ||||
|  | ||||
| # Propagate error "code" upwards | ||||
| if not errors_found: | ||||
|     print('Everything ok') | ||||
| exit(errors_found) | ||||
		Reference in New Issue
	
	Block a user
	 Martin Hořeňovský
					Martin Hořeňovský