test_processes.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #!/usr/bin/env python3
  2. import argparse
  3. import concurrent.futures
  4. import os
  5. import sys
  6. from collections import defaultdict
  7. from tqdm import tqdm
  8. from typing import Any
  9. from openpilot.selfdrive.car.car_helpers import interface_names
  10. from openpilot.tools.lib.openpilotci import get_url, upload_file
  11. from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
  12. from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, check_openpilot_enabled, replay_process
  13. from openpilot.system.version import get_commit
  14. from openpilot.tools.lib.filereader import FileReader
  15. from openpilot.tools.lib.logreader import LogReader
  16. from openpilot.tools.lib.helpers import save_log
  17. source_segments = [
  18. ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY
  19. ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA
  20. ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.KIA_EV6 (+ QCOM GPS)
  21. ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS
  22. ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4
  23. ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2
  24. ("HONDA", "eb140f119469d9ab|2021-06-12--10-46-24--27"), # HONDA.CIVIC (NIDEC)
  25. ("HONDA2", "7d2244f34d1bbcda|2021-06-25--12-25-37--26"), # HONDA.ACCORD (BOSCH)
  26. ("CHRYSLER", "4deb27de11bee626|2021-02-20--11-28-55--8"), # CHRYSLER.PACIFICA_2018_HYBRID
  27. ("RAM", "17fc16d840fe9d21|2023-04-26--13-28-44--5"), # CHRYSLER.RAM_1500
  28. ("SUBARU", "341dccd5359e3c97|2022-09-12--10-35-33--3"), # SUBARU.OUTBACK
  29. ("GM", "0c58b6a25109da2b|2021-02-23--16-35-50--11"), # GM.VOLT
  30. ("GM2", "376bf99325883932|2022-10-27--13-41-22--1"), # GM.BOLT_EUV
  31. ("NISSAN", "35336926920f3571|2021-02-12--18-38-48--46"), # NISSAN.XTRAIL
  32. ("VOLKSWAGEN", "de9592456ad7d144|2021-06-29--11-00-15--6"), # VOLKSWAGEN.GOLF
  33. ("MAZDA", "bd6a637565e91581|2021-10-30--15-14-53--4"), # MAZDA.CX9_2021
  34. ("FORD", "54827bf84c38b14f|2023-01-26--21-59-07--4"), # FORD.BRONCO_SPORT_MK1
  35. # Enable when port is tested and dashcamOnly is no longer set
  36. #("TESLA", "bb50caf5f0945ab1|2021-06-19--17-20-18--3"), # TESLA.AP2_MODELS
  37. #("VOLKSWAGEN2", "3cfdec54aa035f3f|2022-07-19--23-45-10--2"), # VOLKSWAGEN.PASSAT_NMS
  38. ]
  39. segments = [
  40. ("BODY", "regen997DF2697CB|2023-10-30--23-14-29--0"),
  41. ("HYUNDAI", "regen2A9D2A8E0B4|2023-10-30--23-13-34--0"),
  42. ("HYUNDAI2", "regen6CA24BC3035|2023-10-30--23-14-28--0"),
  43. ("TOYOTA", "regen5C019D76307|2023-10-30--23-13-31--0"),
  44. ("TOYOTA2", "regen5DCADA88A96|2023-10-30--23-14-57--0"),
  45. ("TOYOTA3", "regen7204CA3A498|2023-10-30--23-15-55--0"),
  46. ("HONDA", "regen048F8FA0B24|2023-10-30--23-15-53--0"),
  47. ("HONDA2", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"),
  48. ("CHRYSLER", "regen7125C42780C|2023-10-30--23-16-21--0"),
  49. ("RAM", "regen2731F3213D2|2023-10-30--23-18-11--0"),
  50. ("SUBARU", "regen86E4C1B4DDD|2023-10-30--23-18-14--0"),
  51. ("GM", "regenF6393D64745|2023-10-30--23-17-18--0"),
  52. ("GM2", "regen220F830C05B|2023-10-30--23-18-39--0"),
  53. ("NISSAN", "regen4F671F7C435|2023-10-30--23-18-40--0"),
  54. ("VOLKSWAGEN", "regen8BDFE7307A0|2023-10-30--23-19-36--0"),
  55. ("MAZDA", "regen2E9F1A15FD5|2023-10-30--23-20-36--0"),
  56. ("FORD", "regen6D39E54606E|2023-10-30--23-20-54--0"),
  57. ]
  58. # dashcamOnly makes don't need to be tested until a full port is done
  59. excluded_interfaces = ["mock", "tesla"]
  60. BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
  61. REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
  62. EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
  63. def run_test_process(data):
  64. segment, cfg, args, cur_log_fn, ref_log_path, lr_dat = data
  65. res = None
  66. if not args.upload_only:
  67. lr = LogReader.from_bytes(lr_dat)
  68. res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs)
  69. # save logs so we can upload when updating refs
  70. save_log(cur_log_fn, log_msgs)
  71. if args.update_refs or args.upload_only:
  72. print(f'Uploading: {os.path.basename(cur_log_fn)}')
  73. assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}"
  74. upload_file(cur_log_fn, os.path.basename(cur_log_fn))
  75. os.remove(cur_log_fn)
  76. return (segment, cfg.proc_name, res)
  77. def get_log_data(segment):
  78. r, n = segment.rsplit("--", 1)
  79. with FileReader(get_url(r, n)) as f:
  80. return (segment, f.read())
  81. def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=None, ignore_msgs=None):
  82. if ignore_fields is None:
  83. ignore_fields = []
  84. if ignore_msgs is None:
  85. ignore_msgs = []
  86. ref_log_msgs = list(LogReader(ref_log_path))
  87. try:
  88. log_msgs = replay_process(cfg, lr, disable_progress=True)
  89. except Exception as e:
  90. raise Exception("failed on segment: " + segment) from e
  91. # check to make sure openpilot is engaged in the route
  92. if cfg.proc_name == "controlsd":
  93. if not check_openpilot_enabled(log_msgs):
  94. # FIXME: these segments should work, but the replay enabling logic is too brittle
  95. if segment not in ("regen6CA24BC3035|2023-10-30--23-14-28--0", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"):
  96. return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs
  97. try:
  98. return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs
  99. except Exception as e:
  100. return str(e), log_msgs
  101. if __name__ == "__main__":
  102. all_cars = {car for car, _ in segments}
  103. all_procs = {cfg.proc_name for cfg in CONFIGS if cfg.proc_name not in EXCLUDED_PROCS}
  104. cpu_count = os.cpu_count() or 1
  105. parser = argparse.ArgumentParser(description="Regression test to identify changes in a process's output")
  106. parser.add_argument("--whitelist-procs", type=str, nargs="*", default=all_procs,
  107. help="Whitelist given processes from the test (e.g. controlsd)")
  108. parser.add_argument("--whitelist-cars", type=str, nargs="*", default=all_cars,
  109. help="Whitelist given cars from the test (e.g. HONDA)")
  110. parser.add_argument("--blacklist-procs", type=str, nargs="*", default=[],
  111. help="Blacklist given processes from the test (e.g. controlsd)")
  112. parser.add_argument("--blacklist-cars", type=str, nargs="*", default=[],
  113. help="Blacklist given cars from the test (e.g. HONDA)")
  114. parser.add_argument("--ignore-fields", type=str, nargs="*", default=[],
  115. help="Extra fields or msgs to ignore (e.g. carState.events)")
  116. parser.add_argument("--ignore-msgs", type=str, nargs="*", default=[],
  117. help="Msgs to ignore (e.g. carEvents)")
  118. parser.add_argument("--update-refs", action="store_true",
  119. help="Updates reference logs using current commit")
  120. parser.add_argument("--upload-only", action="store_true",
  121. help="Skips testing processes and uploads logs from previous test run")
  122. parser.add_argument("-j", "--jobs", type=int, default=max(cpu_count - 2, 1),
  123. help="Max amount of parallel jobs")
  124. args = parser.parse_args()
  125. tested_procs = set(args.whitelist_procs) - set(args.blacklist_procs)
  126. tested_cars = set(args.whitelist_cars) - set(args.blacklist_cars)
  127. tested_cars = {c.upper() for c in tested_cars}
  128. full_test = (tested_procs == all_procs) and (tested_cars == all_cars) and all(len(x) == 0 for x in (args.ignore_fields, args.ignore_msgs))
  129. upload = args.update_refs or args.upload_only
  130. os.makedirs(os.path.dirname(FAKEDATA), exist_ok=True)
  131. if upload:
  132. assert full_test, "Need to run full test when updating refs"
  133. try:
  134. with open(REF_COMMIT_FN) as f:
  135. ref_commit = f.read().strip()
  136. except FileNotFoundError:
  137. print("Couldn't find reference commit")
  138. sys.exit(1)
  139. cur_commit = get_commit()
  140. if not cur_commit:
  141. raise Exception("Couldn't get current commit")
  142. print(f"***** testing against commit {ref_commit} *****")
  143. # check to make sure all car brands are tested
  144. if full_test:
  145. untested = (set(interface_names) - set(excluded_interfaces)) - {c.lower() for c in tested_cars}
  146. assert len(untested) == 0, f"Cars missing routes: {str(untested)}"
  147. log_paths: defaultdict[str, dict[str, dict[str, str]]] = defaultdict(lambda: defaultdict(dict))
  148. with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool:
  149. if not args.upload_only:
  150. download_segments = [seg for car, seg in segments if car in tested_cars]
  151. log_data: dict[str, LogReader] = {}
  152. p1 = pool.map(get_log_data, download_segments)
  153. for segment, lr in tqdm(p1, desc="Getting Logs", total=len(download_segments)):
  154. log_data[segment] = lr
  155. pool_args: Any = []
  156. for car_brand, segment in segments:
  157. if car_brand not in tested_cars:
  158. continue
  159. for cfg in CONFIGS:
  160. if cfg.proc_name not in tested_procs:
  161. continue
  162. cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.bz2")
  163. if args.update_refs: # reference logs will not exist if routes were just regenerated
  164. ref_log_path = get_url(*segment.rsplit("--", 1))
  165. else:
  166. ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.bz2")
  167. ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn)
  168. dat = None if args.upload_only else log_data[segment]
  169. pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat))
  170. log_paths[segment][cfg.proc_name]['ref'] = ref_log_path
  171. log_paths[segment][cfg.proc_name]['new'] = cur_log_fn
  172. results: Any = defaultdict(dict)
  173. p2 = pool.map(run_test_process, pool_args)
  174. for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)):
  175. if not args.upload_only:
  176. results[segment][proc] = result
  177. diff_short, diff_long, failed = format_diff(results, log_paths, ref_commit)
  178. if not upload:
  179. with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f:
  180. f.write(diff_long)
  181. print(diff_short)
  182. if failed:
  183. print("TEST FAILED")
  184. print("\n\nTo push the new reference logs for this commit run:")
  185. print("./test_processes.py --upload-only")
  186. else:
  187. print("TEST SUCCEEDED")
  188. else:
  189. with open(REF_COMMIT_FN, "w") as f:
  190. f.write(cur_commit)
  191. print(f"\n\nUpdated reference logs for commit: {cur_commit}")
  192. sys.exit(int(failed))