checkConvenienceHeaders.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env python3
  2. """
  3. Checks that all of the "catch_foo_all.hpp" headers include all subheaders.
  4. The logic is simple: given a folder, e.g. `catch2/matchers`, then the
  5. ccorresponding header is called `catch_matchers_all.hpp` and contains
  6. * all headers in `catch2/matchers`,
  7. * all headers in `catch2/matchers/{internal, detail}`,
  8. * all convenience catch_matchers_*_all.hpp headers from any non-internal subfolders
  9. The top level header is called `catch_all.hpp`.
  10. """
  11. internal_dirs = ['detail', 'internal']
  12. from scriptCommon import catchPath
  13. from glob import glob
  14. from pprint import pprint
  15. import os
  16. import re
  17. def normalized_path(path):
  18. """Replaces \ in paths on Windows with /"""
  19. return path.replace('\\', '/')
  20. def normalized_paths(paths):
  21. """Replaces \ with / in every path"""
  22. return [normalized_path(path) for path in paths]
  23. source_path = catchPath + '/src/catch2'
  24. source_path = normalized_path(source_path)
  25. include_parser = re.compile(r'#include <(catch2/.+\.hpp)>')
  26. errors_found = False
  27. def headers_in_folder(folder):
  28. return glob(folder + '/*.hpp')
  29. def folders_in_folder(folder):
  30. return [x for x in os.scandir(folder) if x.is_dir()]
  31. def collated_includes(folder):
  32. base = headers_in_folder(folder)
  33. for subfolder in folders_in_folder(folder):
  34. if subfolder.name in internal_dirs:
  35. base.extend(headers_in_folder(subfolder.path))
  36. else:
  37. base.append(subfolder.path + '/catch_{}_all.hpp'.format(subfolder.name))
  38. return normalized_paths(sorted(base))
  39. def includes_from_file(header):
  40. includes = []
  41. with open(header, 'r', encoding = 'utf-8') as file:
  42. for line in file:
  43. if not line.startswith('#include'):
  44. continue
  45. match = include_parser.match(line)
  46. if match:
  47. includes.append(match.group(1))
  48. return normalized_paths(includes)
  49. def normalize_includes(includes):
  50. """Returns """
  51. return [include[len(catchPath)+5:] for include in includes]
  52. def get_duplicates(xs):
  53. seen = set()
  54. duplicated = []
  55. for x in xs:
  56. if x in seen:
  57. duplicated.append(x)
  58. seen.add(x)
  59. return duplicated
  60. def verify_convenience_header(folder):
  61. """
  62. Performs the actual checking of convenience header for specific folder.
  63. Checks that
  64. 1) The header even exists
  65. 2) That all includes in the header are sorted
  66. 3) That there are no duplicated includes
  67. 4) That all includes that should be in the header are actually present in the header
  68. 5) That there are no superfluous includes that should not be in the header
  69. """
  70. global errors_found
  71. path = normalized_path(folder.path)
  72. assert path.startswith(source_path), '{} does not start with {}'.format(path, source_path)
  73. stripped_path = path[len(source_path) + 1:]
  74. path_pieces = stripped_path.split('/')
  75. if path == source_path:
  76. header_name = 'catch_all.hpp'
  77. else:
  78. header_name = 'catch_{}_all.hpp'.format('_'.join(path_pieces))
  79. # 1) Does it exist?
  80. full_path = path + '/' + header_name
  81. if not os.path.isfile(full_path):
  82. errors_found = True
  83. print('Missing convenience header: {}'.format(full_path))
  84. return
  85. file_incs = includes_from_file(path + '/' + header_name)
  86. # 2) Are the includes are sorted?
  87. if sorted(file_incs) != file_incs:
  88. errors_found = True
  89. print("'{}': Includes are not in sorted order!".format(header_name))
  90. # 3) Are there no duplicates?
  91. duplicated = get_duplicates(file_incs)
  92. for duplicate in duplicated:
  93. errors_found = True
  94. print("'{}': Duplicated include: '{}'".format(header_name, duplicate))
  95. target_includes = normalize_includes(collated_includes(path))
  96. # Avoid requiring the convenience header to include itself
  97. target_includes = [x for x in target_includes if header_name not in x]
  98. # 4) Are all required headers present?
  99. file_incs_set = set(file_incs)
  100. for include in target_includes:
  101. if (include not in file_incs_set and
  102. include != 'catch2/internal/catch_windows_h_proxy.hpp'):
  103. errors_found = True
  104. print("'{}': missing include '{}'".format(header_name, include))
  105. # 5) Are there any superfluous headers?
  106. desired_set = set(target_includes)
  107. for include in file_incs:
  108. if include not in desired_set:
  109. errors_found = True
  110. print("'{}': superfluous include '{}'".format(header_name, include))
  111. def walk_source_folders(current):
  112. verify_convenience_header(current)
  113. for folder in folders_in_folder(current.path):
  114. fname = folder.name
  115. if fname not in internal_dirs:
  116. walk_source_folders(folder)
  117. # This is an ugly hack because we cannot instantiate DirEntry manually
  118. base_dir = [x for x in os.scandir(catchPath + '/src') if x.name == 'catch2']
  119. walk_source_folders(base_dir[0])
  120. # Propagate error "code" upwards
  121. if not errors_found:
  122. print('Everything ok')
  123. exit(errors_found)