mars_utils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import datetime
  9. import json
  10. import socket
  11. from tiaf_logger import get_logger
  12. import tiaf_report_constants as constants
  13. logger = get_logger(__file__)
  14. class FilebeatExn(Exception):
  15. pass
  16. class FilebeatClient(object):
  17. def __init__(self, host="127.0.0.1", port=9000, timeout=20):
  18. self._filebeat_host = host
  19. self._filebeat_port = port
  20. self._socket_timeout = timeout
  21. self._socket = None
  22. self._open_socket()
  23. def send_event(self, payload, index, timestamp=None, pipeline="filebeat"):
  24. if not timestamp:
  25. timestamp = datetime.datetime.utcnow().timestamp()
  26. event = {
  27. "index": index,
  28. "timestamp": timestamp,
  29. "pipeline": pipeline,
  30. "payload": json.dumps(payload)
  31. }
  32. # Serialise event, add new line and encode as UTF-8 before sending to Filebeat.
  33. data = json.dumps(event, sort_keys=True) + "\n"
  34. data = data.encode()
  35. #print(f"-> {data}")
  36. self._send_data(data)
  37. def _open_socket(self):
  38. logger.info(f"Connecting to Filebeat on {self._filebeat_host}:{self._filebeat_port}")
  39. self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  40. self._socket.settimeout(self._socket_timeout)
  41. try:
  42. self._socket.connect((self._filebeat_host, self._filebeat_port))
  43. except (ConnectionError, socket.timeout):
  44. raise FilebeatExn("Failed to connect to Filebeat") from None
  45. def _send_data(self, data):
  46. total_sent = 0
  47. while total_sent < len(data):
  48. try:
  49. sent = self._socket.send(data[total_sent:])
  50. except BrokenPipeError:
  51. logger.error("Filebeat socket closed by peer")
  52. self._socket.close()
  53. self._open_socket()
  54. total_sent = 0
  55. else:
  56. total_sent = total_sent + sent
  57. def format_timestamp(timestamp: float):
  58. """
  59. Formats the given floating point timestamp into "yyyy-MM-dd'T'HH:mm:ss.SSSXX" format.
  60. @param timestamp: The timestamp to format.
  61. @return: The formatted timestamp.
  62. """
  63. return datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
  64. def generate_mars_timestamp(t0_offset_milliseconds: int, t0_timestamp: float):
  65. """
  66. Generates a MARS timestamp in the format "yyyy-MM-dd'T'HH:mm:ss.SSSXX" by offsetting the T0 timestamp
  67. by the specified amount of milliseconds.
  68. @param t0_offset_milliseconds: The amount of time to offset from T0.
  69. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  70. @return: The formatted timestamp offset from T0 by the specified amount of milliseconds.
  71. """
  72. t0_offset_seconds = get_duration_in_seconds(t0_offset_milliseconds)
  73. t0_offset_timestamp = t0_timestamp + t0_offset_seconds
  74. return format_timestamp(t0_offset_timestamp)
  75. def get_duration_in_seconds(duration_in_milliseconds: int):
  76. """
  77. Gets the specified duration in milliseconds (as used by TIAF) in seconds (as used my MARS documents).
  78. @param duration_in_milliseconds: The millisecond duration to transform into seconds.
  79. @return: The duration in seconds.
  80. """
  81. return duration_in_milliseconds * 0.001
  82. def generate_mars_job(tiaf_result, driver_args, build_number: int):
  83. """
  84. Generates a MARS job document using the job meta-data used to drive the TIAF sequence.
  85. @param tiaf_result: The result object generated by the TIAF script.
  86. @param driver_args: The arguments specified to the driver script.
  87. @param driver_args: The arguments specified to the driver script.
  88. @param build_number: The build number this job corresponds to.
  89. @return: The MARS job document with the job meta-data.
  90. """
  91. mars_job = {key:tiaf_result.get(key, None) for key in
  92. [
  93. constants.SRC_COMMIT_KEY,
  94. constants.DST_COMMIT_KEY,
  95. constants.COMMIT_DISTANCE_KEY,
  96. constants.SRC_BRANCH_KEY,
  97. constants.DST_BRANCH_KEY,
  98. constants.SUITES_KEY,
  99. constants.LABEL_EXCLUDES_KEY,
  100. constants.SOURCE_OF_TRUTH_BRANCH_KEY,
  101. constants.IS_SOURCE_OF_TRUTH_BRANCH_KEY,
  102. constants.USE_TEST_IMPACT_ANALYSIS_KEY,
  103. constants.HAS_CHANGE_LIST_KEY,
  104. constants.HAS_HISTORIC_DATA_KEY,
  105. constants.S3_BUCKET_KEY,
  106. constants.RUNTIME_ARGS_KEY,
  107. constants.RUNTIME_RETURN_CODE_KEY,
  108. constants.RUNTIME_TYPE_KEY,
  109. constants.MISMATCHED_TESTS_KEY,
  110. constants.MISMATCHED_TESTS_COUNT_KEY
  111. ]}
  112. mars_job[constants.DRIVER_ARGS_KEY] = driver_args
  113. mars_job[constants.BUILD_NUMBER_KEY] = build_number
  114. return mars_job
  115. def generate_test_run_list(test_runs):
  116. """
  117. Generates a list of test run name strings from the list of TIAF test runs.
  118. @param test_runs: The list of TIAF test runs to generate the name strings from.
  119. @return: The list of test run name strings.
  120. """
  121. test_run_list = []
  122. for test_run in test_runs:
  123. test_run_list.append(test_run[constants.NAME_KEY])
  124. return test_run_list
  125. def generate_mars_test_run_selections(test_run_selection, test_run_report, t0_timestamp: float):
  126. """
  127. Generates a list of MARS test run selections from a TIAF test run selection and report.
  128. @param test_run_selection: The TIAF test run selection.
  129. @param test_run_report: The TIAF test run report.
  130. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  131. @return: The list of TIAF test runs.
  132. """
  133. mars_test_run_selection = {key:test_run_report.get(key, None) for key in
  134. [
  135. constants.RESULT_KEY,
  136. constants.NUM_PASSING_TEST_RUNS_KEY,
  137. constants.NUM_FAILING_TEST_RUNS_KEY,
  138. constants.NUM_EXECUTION_FAILURE_TEST_RUNS_KEY,
  139. constants.NUM_TIMED_OUT_TEST_RUNS_KEY,
  140. constants.NUM_UNEXECUTED_TEST_RUNS_KEY,
  141. constants.TOTAL_NUM_PASSING_TESTS_KEY,
  142. constants.TOTAL_NUM_FAILING_TESTS_KEY,
  143. constants.TOTAL_NUM_DISABLED_TESTS_KEY
  144. ]}
  145. mars_test_run_selection[constants.START_TIME_KEY] = generate_mars_timestamp(test_run_report[constants.START_TIME_KEY], t0_timestamp)
  146. mars_test_run_selection[constants.END_TIME_KEY] = generate_mars_timestamp(test_run_report[constants.END_TIME_KEY], t0_timestamp)
  147. mars_test_run_selection[constants.DURATION_KEY] = get_duration_in_seconds(test_run_report[constants.DURATION_KEY])
  148. mars_test_run_selection[constants.INCLUDED_TEST_RUNS_KEY] = test_run_selection[constants.INCLUDED_TEST_RUNS_KEY]
  149. mars_test_run_selection[constants.EXCLUDED_TEST_RUNS_KEY] = test_run_selection[constants.EXCLUDED_TEST_RUNS_KEY]
  150. mars_test_run_selection[constants.NUM_INCLUDED_TEST_RUNS_KEY] = test_run_selection[constants.NUM_INCLUDED_TEST_RUNS_KEY]
  151. mars_test_run_selection[constants.NUM_EXCLUDED_TEST_RUNS_KEY] = test_run_selection[constants.NUM_EXCLUDED_TEST_RUNS_KEY]
  152. mars_test_run_selection[constants.TOTAL_NUM_TEST_RUNS_KEY] = test_run_selection[constants.TOTAL_NUM_TEST_RUNS_KEY]
  153. mars_test_run_selection[constants.PASSING_TEST_RUNS_KEY] = generate_test_run_list(test_run_report[constants.PASSING_TEST_RUNS_KEY])
  154. mars_test_run_selection[constants.FAILING_TEST_RUNS_KEY] = generate_test_run_list(test_run_report[constants.FAILING_TEST_RUNS_KEY])
  155. mars_test_run_selection[constants.EXECUTION_FAILURE_TEST_RUNS_KEY] = generate_test_run_list(test_run_report[constants.EXECUTION_FAILURE_TEST_RUNS_KEY])
  156. mars_test_run_selection[constants.TIMED_OUT_TEST_RUNS_KEY] = generate_test_run_list(test_run_report[constants.TIMED_OUT_TEST_RUNS_KEY])
  157. mars_test_run_selection[constants.UNEXECUTED_TEST_RUNS_KEY] = generate_test_run_list(test_run_report[constants.UNEXECUTED_TEST_RUNS_KEY])
  158. return mars_test_run_selection
  159. def generate_test_runs_from_list(test_run_list: list):
  160. """
  161. Generates a list of TIAF test runs from a list of test target name strings.
  162. @param test_run_list: The list of test target names.
  163. @return: The list of TIAF test runs.
  164. """
  165. test_run_list = {
  166. constants.TOTAL_NUM_TEST_RUNS_KEY: len(test_run_list),
  167. constants.NUM_INCLUDED_TEST_RUNS_KEY: len(test_run_list),
  168. constants.NUM_EXCLUDED_TEST_RUNS_KEY: 0,
  169. constants.INCLUDED_TEST_RUNS_KEY: test_run_list,
  170. constants.EXCLUDED_TEST_RUNS_KEY: []
  171. }
  172. return test_run_list
  173. def generate_mars_sequence(sequence_report: dict, mars_job: dict, change_list:dict, t0_timestamp: float):
  174. """
  175. Generates the MARS sequence document from the specified TIAF sequence report.
  176. @param sequence_report: The TIAF runtime sequence report.
  177. @param mars_job: The MARS job for this sequence.
  178. @param change_list: The change list for which the TIAF sequence was run.
  179. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  180. @return: The MARS sequence document for the specified TIAF sequence report.
  181. """
  182. mars_sequence = {key:sequence_report.get(key, None) for key in
  183. [
  184. constants.SEQUENCE_TYPE_KEY,
  185. constants.RESULT_KEY,
  186. constants.POLICY_KEY,
  187. constants.TOTAL_NUM_TEST_RUNS_KEY,
  188. constants.TOTAL_NUM_PASSING_TEST_RUNS_KEY,
  189. constants.TOTAL_NUM_FAILING_TEST_RUNS_KEY,
  190. constants.TOTAL_NUM_EXECUTION_FAILURE_TEST_RUNS_KEY,
  191. constants.TOTAL_NUM_TIMED_OUT_TEST_RUNS_KEY,
  192. constants.TOTAL_NUM_UNEXECUTED_TEST_RUNS_KEY,
  193. constants.TOTAL_NUM_PASSING_TESTS_KEY,
  194. constants.TOTAL_NUM_FAILING_TESTS_KEY,
  195. constants.TOTAL_NUM_DISABLED_TESTS_KEY
  196. ]}
  197. mars_sequence[constants.START_TIME_KEY] = generate_mars_timestamp(sequence_report[constants.START_TIME_KEY], t0_timestamp)
  198. mars_sequence[constants.END_TIME_KEY] = generate_mars_timestamp(sequence_report[constants.END_TIME_KEY], t0_timestamp)
  199. mars_sequence[constants.DURATION_KEY] = get_duration_in_seconds(sequence_report[constants.DURATION_KEY])
  200. config = {key:sequence_report[key] for key in
  201. [
  202. constants.TEST_TARGET_TIMEOUT_KEY,
  203. constants.GLOBAL_TIMEOUT_KEY,
  204. constants.MAX_CONCURRENCY_KEY
  205. ]}
  206. test_run_selection = {}
  207. test_run_selection[constants.SELECTED_KEY] = generate_mars_test_run_selections(sequence_report[constants.SELECTED_TEST_RUNS_KEY], sequence_report[constants.SELECTED_TEST_RUN_REPORT_KEY], t0_timestamp)
  208. if sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY or sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SAFE_IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY:
  209. total_test_runs = sequence_report[constants.TOTAL_NUM_TEST_RUNS_KEY] + len(sequence_report[constants.DISCARDED_TEST_RUNS_KEY])
  210. if total_test_runs > 0:
  211. test_run_selection[constants.SELECTED_KEY][constants.EFFICIENCY_KEY] = (1.0 - (test_run_selection[constants.SELECTED_KEY][constants.TOTAL_NUM_TEST_RUNS_KEY] / total_test_runs)) * 100
  212. else:
  213. test_run_selection[constants.SELECTED_KEY][constants.EFFICIENCY_KEY] = 100
  214. test_run_selection[constants.DRAFTED_KEY] = generate_mars_test_run_selections(generate_test_runs_from_list(sequence_report[constants.DRAFTED_TEST_RUNS_KEY]), sequence_report[constants.DRAFTED_TEST_RUN_REPORT_KEY], t0_timestamp)
  215. if sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SAFE_IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY:
  216. test_run_selection[constants.DISCARDED_KEY] = generate_mars_test_run_selections(sequence_report[constants.DISCARDED_TEST_RUNS_KEY], sequence_report[constants.DISCARDED_TEST_RUN_REPORT_KEY], t0_timestamp)
  217. else:
  218. test_run_selection[constants.SELECTED_KEY][constants.EFFICIENCY_KEY] = 0
  219. mars_sequence[constants.MARS_JOB_KEY] = mars_job
  220. mars_sequence[constants.CONFIG_KEY] = config
  221. mars_sequence[constants.TEST_RUN_SELECTION_KEY] = test_run_selection
  222. mars_sequence[constants.CHANGE_LIST_KEY] = change_list
  223. return mars_sequence
  224. def extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp: float):
  225. """
  226. Extracts a MARS test target from the specified TIAF test run.
  227. @param test_run: The TIAF test run.
  228. @param instrumentation: Flag specifying whether or not instrumentation was used for the test targets in this run.
  229. @param mars_job: The MARS job for this test target.
  230. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  231. @return: The MARS test target documents for the specified TIAF test target.
  232. """
  233. mars_test_run = {key:test_run.get(key, None) for key in
  234. [
  235. constants.NAME_KEY,
  236. constants.RESULT_KEY,
  237. constants.NUM_PASSING_TESTS_KEY,
  238. constants.NUM_FAILING_TESTS_KEY,
  239. constants.NUM_DISABLED_TESTS_KEY,
  240. constants.COMMAND_ARGS_STRING
  241. ]}
  242. mars_test_run[constants.START_TIME_KEY] = generate_mars_timestamp(test_run[constants.START_TIME_KEY], t0_timestamp)
  243. mars_test_run[constants.END_TIME_KEY] = generate_mars_timestamp(test_run[constants.END_TIME_KEY], t0_timestamp)
  244. mars_test_run[constants.DURATION_KEY] = get_duration_in_seconds(test_run[constants.DURATION_KEY])
  245. mars_test_run[constants.MARS_JOB_KEY] = mars_job
  246. mars_test_run[constants.INSTRUMENTATION_KEY] = instrumentation
  247. return mars_test_run
  248. def extract_mars_test_targets_from_report(test_run_report, instrumentation, mars_job, t0_timestamp: float):
  249. """
  250. Extracts the MARS test targets from the specified TIAF test run report.
  251. @param test_run_report: The TIAF runtime test run report.
  252. @param instrumentation: Flag specifying whether or not instrumentation was used for the test targets in this run.
  253. @param mars_job: The MARS job for these test targets.
  254. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  255. @return: The list of all MARS test target documents for the test targets in the TIAF test run report.
  256. """
  257. mars_test_targets = []
  258. for test_run in test_run_report[constants.PASSING_TEST_RUNS_KEY]:
  259. mars_test_targets.append(extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp))
  260. for test_run in test_run_report[constants.FAILING_TEST_RUNS_KEY]:
  261. mars_test_targets.append(extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp))
  262. for test_run in test_run_report[constants.EXECUTION_FAILURE_TEST_RUNS_KEY]:
  263. mars_test_targets.append(extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp))
  264. for test_run in test_run_report[constants.TIMED_OUT_TEST_RUNS_KEY]:
  265. mars_test_targets.append(extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp))
  266. for test_run in test_run_report[constants.UNEXECUTED_TEST_RUNS_KEY]:
  267. mars_test_targets.append(extract_mars_test_target(test_run, instrumentation, mars_job, t0_timestamp))
  268. return mars_test_targets
  269. def generate_mars_test_targets(sequence_report: dict, mars_job: dict, t0_timestamp: float):
  270. """
  271. Generates a MARS test target document for each test target in the TIAF sequence report.
  272. @param sequence_report: The TIAF runtime sequence report.
  273. @param mars_job: The MARS job for this sequence.
  274. @param t0_timestamp: The T0 timestamp that TIAF timings will be offst from.
  275. @return: The list of all MARS test target documents for the test targets in the TIAF sequence report.
  276. """
  277. mars_test_targets = []
  278. # Determine whether or not the test targets were executed with instrumentation
  279. if sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SEED_SEQUENCE_TYPE_KEY or sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SAFE_IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY or (sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY and sequence_report[constants.POLICY_KEY][constants.DYNAMIC_DEPENDENCY_MAP_POLICY_KEY] == constants.DYNAMIC_DEPENDENCY_MAP_POLICY_UPDATE_KEY):
  280. instrumentation = True
  281. else:
  282. instrumentation = False
  283. # Extract the MARS test target documents from each of the test run reports
  284. mars_test_targets += extract_mars_test_targets_from_report(sequence_report[constants.SELECTED_TEST_RUN_REPORT_KEY], instrumentation, mars_job, t0_timestamp)
  285. if sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY or sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SAFE_IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY:
  286. mars_test_targets += extract_mars_test_targets_from_report(sequence_report[constants.DRAFTED_TEST_RUN_REPORT_KEY], instrumentation, mars_job, t0_timestamp)
  287. if sequence_report[constants.SEQUENCE_TYPE_KEY] == constants.SAFE_IMPACT_ANALYSIS_SEQUENCE_TYPE_KEY:
  288. mars_test_targets += extract_mars_test_targets_from_report(sequence_report[constants.DISCARDED_TEST_RUN_REPORT_KEY], instrumentation, mars_job, t0_timestamp)
  289. return mars_test_targets
  290. def transmit_report_to_mars(mars_index_prefix: str, tiaf_result: dict, driver_args: list, build_number: int):
  291. """
  292. Transforms the TIAF result into the appropriate MARS documents and transmits them to MARS.
  293. @param mars_index_prefix: The index prefix to be used for all MARS documents.
  294. @param tiaf_result: The result object from the TIAF script.
  295. @param driver_args: The arguments passed to the TIAF driver script.
  296. """
  297. try:
  298. filebeat = FilebeatClient("localhost", 9000, 60)
  299. # T0 is the current timestamp that the report timings will be offset from
  300. t0_timestamp = datetime.datetime.now().timestamp()
  301. # Generate and transmit the MARS job document
  302. mars_job = generate_mars_job(tiaf_result, driver_args, build_number)
  303. filebeat.send_event(mars_job, f"{mars_index_prefix}.tiaf.job")
  304. if tiaf_result[constants.REPORT_KEY]:
  305. # Generate and transmit the MARS sequence document
  306. mars_sequence = generate_mars_sequence(tiaf_result[constants.REPORT_KEY], mars_job, tiaf_result[constants.CHANGE_LIST_KEY], t0_timestamp)
  307. filebeat.send_event(mars_sequence, f"{mars_index_prefix}.tiaf.sequence")
  308. # Generate and transmit the MARS test target documents
  309. mars_test_targets = generate_mars_test_targets(tiaf_result[constants.REPORT_KEY], mars_job, t0_timestamp)
  310. for mars_test_target in mars_test_targets:
  311. filebeat.send_event(mars_test_target, f"{mars_index_prefix}.tiaf.test_target")
  312. except FilebeatExn as e:
  313. logger.error(e)
  314. except KeyError as e:
  315. logger.error(f"The report does not contain the key {str(e)}.")