test_summary.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. from __future__ import annotations
  10. from dataclasses import dataclass, field
  11. from materialize.mzcompose.test_result import TestFailureDetails
  12. from materialize.output_consistency.expression.expression import Expression
  13. from materialize.output_consistency.expression.expression_with_args import (
  14. ExpressionWithArgs,
  15. )
  16. from materialize.output_consistency.operation.operation import DbOperationOrFunction
  17. from materialize.output_consistency.output.reproduction_code_printer import (
  18. ReproductionCodePrinter,
  19. )
  20. from materialize.output_consistency.query.query_template import QueryTemplate
  21. from materialize.output_consistency.status.consistency_test_logger import (
  22. ConsistencyTestLogger,
  23. )
  24. from materialize.output_consistency.validation.validation_outcome import (
  25. ValidationOutcome,
  26. ValidationVerdict,
  27. )
  28. @dataclass
  29. class DbOperationOrFunctionStats:
  30. count_top_level_expression_generated: int = 0
  31. count_nested_expression_generated: int = 0
  32. count_expression_generation_failed: int = 0
  33. count_included_in_executed_queries: int = 0
  34. count_included_in_successfully_executed_queries: int = 0
  35. def to_description(self) -> str:
  36. if self.count_included_in_successfully_executed_queries:
  37. success_experienced_info = "successfully executed at least once"
  38. else:
  39. count_generated = (
  40. self.count_top_level_expression_generated
  41. + self.count_nested_expression_generated
  42. )
  43. if count_generated == 0:
  44. success_experienced_info = "expression never generated"
  45. elif self.count_included_in_executed_queries == 0:
  46. success_experienced_info = "query with this expression never generated"
  47. elif self.count_included_in_executed_queries < 15:
  48. success_experienced_info = "not included in any query that was successfully executed in all strategies!"
  49. else:
  50. success_experienced_info = "not included in any query that was successfully executed in all strategies (possibly invalid operation specification)!"
  51. return (
  52. f"{self.count_top_level_expression_generated} top level, "
  53. f"{self.count_nested_expression_generated} nested, "
  54. f"{self.count_expression_generation_failed} generation failed, "
  55. f"{success_experienced_info}"
  56. )
  57. def merge(self, other: DbOperationOrFunctionStats) -> None:
  58. self.count_top_level_expression_generated = (
  59. self.count_top_level_expression_generated
  60. + other.count_top_level_expression_generated
  61. )
  62. self.count_nested_expression_generated = (
  63. self.count_nested_expression_generated
  64. + other.count_nested_expression_generated
  65. )
  66. self.count_expression_generation_failed = (
  67. self.count_expression_generation_failed
  68. + other.count_expression_generation_failed
  69. )
  70. self.count_included_in_executed_queries = (
  71. self.count_included_in_executed_queries
  72. + other.count_included_in_executed_queries
  73. )
  74. self.count_included_in_successfully_executed_queries = (
  75. self.count_included_in_successfully_executed_queries
  76. + other.count_included_in_successfully_executed_queries
  77. )
  78. @dataclass
  79. class DbOperationVariant:
  80. operation: DbOperationOrFunction
  81. param_count: int
  82. def to_description(self) -> str:
  83. return self.operation.to_description(self.param_count)
  84. def __hash__(self):
  85. return hash(self.to_description())
  86. @dataclass
  87. class ConsistencyTestSummary(ConsistencyTestLogger):
  88. """Summary of the test execution"""
  89. dry_run: bool = False
  90. mode: str = "UNKNOWN"
  91. count_executed_query_templates: int = 0
  92. count_successful_query_templates: int = 0
  93. count_ignored_error_query_templates: int = 0
  94. count_with_warning_query_templates: int = 0
  95. failures: list[TestFailureDetails] = field(default_factory=list)
  96. stats_by_operation_variant: dict[DbOperationVariant, DbOperationOrFunctionStats] = (
  97. field(default_factory=dict)
  98. )
  99. count_available_data_types: int = 0
  100. count_available_op_variants: int = 0
  101. count_predefined_queries: int = 0
  102. count_generated_select_expressions: int = 0
  103. count_ignored_select_expressions: int = 0
  104. used_ignore_reasons: set[str] = field(default_factory=set)
  105. def __post_init__(self):
  106. self.mode = "LIVE_DATABASE" if not self.dry_run else "DRY_RUN"
  107. def count_failures(self) -> int:
  108. return len(self.failures)
  109. def merge(self, other: ConsistencyTestSummary) -> None:
  110. assert self.dry_run == other.dry_run
  111. assert self.mode == other.mode
  112. self.count_executed_query_templates = (
  113. self.count_executed_query_templates + other.count_executed_query_templates
  114. )
  115. self.count_successful_query_templates = (
  116. self.count_successful_query_templates
  117. + other.count_successful_query_templates
  118. )
  119. self.count_ignored_error_query_templates = (
  120. self.count_ignored_error_query_templates
  121. + other.count_ignored_error_query_templates
  122. )
  123. self.count_with_warning_query_templates = (
  124. self.count_with_warning_query_templates
  125. + other.count_with_warning_query_templates
  126. )
  127. self.failures.extend(other.failures)
  128. for operation_variant, other_stats in other.stats_by_operation_variant.items():
  129. stats = self.stats_by_operation_variant.get(operation_variant)
  130. if stats is None:
  131. self.stats_by_operation_variant[operation_variant] = other_stats
  132. else:
  133. stats.merge(other_stats)
  134. self.count_available_data_types = max(
  135. self.count_available_data_types, other.count_available_data_types
  136. )
  137. self.count_available_op_variants = max(
  138. self.count_available_op_variants, other.count_available_op_variants
  139. )
  140. self.count_predefined_queries = max(
  141. self.count_predefined_queries, other.count_predefined_queries
  142. )
  143. self.count_generated_select_expressions = (
  144. self.count_generated_select_expressions
  145. + other.count_generated_select_expressions
  146. )
  147. self.count_ignored_select_expressions = (
  148. self.count_ignored_select_expressions
  149. + other.count_ignored_select_expressions
  150. )
  151. def add_failures(self, failures: list[TestFailureDetails]) -> None:
  152. self.failures.extend(failures)
  153. def record_ignore_reason_usage(self, reason: str) -> None:
  154. self.used_ignore_reasons.add(reason)
  155. def all_passed(self) -> bool:
  156. all_passed = (
  157. self.count_executed_query_templates
  158. == self.count_successful_query_templates
  159. + self.count_ignored_error_query_templates
  160. )
  161. assert all_passed == (len(self.failures) == 0)
  162. return all_passed
  163. def get(self) -> str:
  164. count_accepted_queries = (
  165. self.count_successful_query_templates
  166. + self.count_ignored_error_query_templates
  167. )
  168. count_ok = count_accepted_queries
  169. count_all = self.count_executed_query_templates
  170. percentage = 100 * count_ok / count_all if count_all > 0 else 0
  171. output_rows = [
  172. f"{count_ok}/{count_all} ({round(percentage, 2)}%) queries passed"
  173. f" in mode '{self.mode}'.",
  174. f"{self.count_ignored_error_query_templates} queries were ignored after execution.",
  175. f"{self.count_with_warning_query_templates} queries had warnings.",
  176. ]
  177. output_rows.extend(self._get_global_warning_rows())
  178. return "\n".join(output_rows)
  179. def _get_global_warning_rows(self) -> list[str]:
  180. if len(self.global_warnings) == 0:
  181. return []
  182. unique_warnings_with_count = dict()
  183. for warning in self.global_warnings:
  184. unique_warnings_with_count[warning] = 1 + (
  185. unique_warnings_with_count.get(warning) or 0
  186. )
  187. unique_global_warnings = [
  188. f"{warning} ({count} occurrences)"
  189. for warning, count in unique_warnings_with_count.items()
  190. ]
  191. unique_global_warnings.sort()
  192. warning_rows = [
  193. f"{len(unique_global_warnings)} unique, non-query specific warnings occurred:"
  194. ]
  195. for warning in unique_global_warnings:
  196. warning_rows.append(f"* {warning}")
  197. return warning_rows
  198. def get_function_and_operation_stats(self) -> str:
  199. output = []
  200. for (
  201. operation_variant,
  202. stats,
  203. ) in self.stats_by_operation_variant.items():
  204. output.append(
  205. f"* {operation_variant.to_description()}: {stats.to_description()}"
  206. )
  207. output.sort()
  208. return "\n".join(output)
  209. def format_used_ignore_entries(self) -> str:
  210. output = []
  211. for ignore_reason in self.used_ignore_reasons:
  212. output.append(f"* {ignore_reason}")
  213. output.sort()
  214. return "\n".join(output)
  215. def count_used_ops(self) -> int:
  216. return len(self.stats_by_operation_variant)
  217. def accept_expression_generation_statistics(
  218. self,
  219. operation: DbOperationOrFunction,
  220. expression: ExpressionWithArgs | None,
  221. number_of_args: int,
  222. is_top_level: bool = True,
  223. ) -> None:
  224. operation_variant = DbOperationVariant(operation, number_of_args)
  225. stats = self.stats_by_operation_variant.get(operation_variant)
  226. if stats is None:
  227. stats = DbOperationOrFunctionStats()
  228. self.stats_by_operation_variant[operation_variant] = stats
  229. if expression is None:
  230. assert is_top_level, "expressions at nested levels must not be None"
  231. stats.count_expression_generation_failed = (
  232. stats.count_expression_generation_failed + 1
  233. )
  234. return
  235. if is_top_level:
  236. stats.count_top_level_expression_generated = (
  237. stats.count_top_level_expression_generated + 1
  238. )
  239. else:
  240. stats.count_nested_expression_generated = (
  241. stats.count_nested_expression_generated + 1
  242. )
  243. for arg in expression.args:
  244. if isinstance(arg, ExpressionWithArgs):
  245. self.accept_expression_generation_statistics(
  246. operation=arg.operation,
  247. expression=arg,
  248. number_of_args=arg.count_args(),
  249. is_top_level=False,
  250. )
  251. def accept_execution_result(
  252. self,
  253. query: QueryTemplate,
  254. test_outcome: ValidationOutcome,
  255. reproduction_code_printer: ReproductionCodePrinter,
  256. ) -> None:
  257. self.count_executed_query_templates += 1
  258. verdict = test_outcome.verdict()
  259. if verdict in {
  260. ValidationVerdict.SUCCESS,
  261. ValidationVerdict.SUCCESS_WITH_WARNINGS,
  262. }:
  263. self.count_successful_query_templates += 1
  264. elif verdict == ValidationVerdict.IGNORED_FAILURE:
  265. self.count_ignored_error_query_templates += 1
  266. elif verdict == ValidationVerdict.FAILURE:
  267. self.add_failures(
  268. test_outcome.to_failure_details(reproduction_code_printer)
  269. )
  270. else:
  271. raise RuntimeError(f"Unexpected verdict: {verdict}")
  272. if test_outcome.has_warnings():
  273. self.count_with_warning_query_templates += 1
  274. self._accept_executed_query(
  275. query, test_outcome.query_execution_succeeded_in_all_strategies
  276. )
  277. def _accept_executed_query(
  278. self, query: QueryTemplate, successfully_executed_in_all_strategies: bool
  279. ) -> None:
  280. # only consider expressions in the SELECT part for now
  281. for expression in query.select_expressions:
  282. self._accept_expression_in_executed_query(
  283. expression,
  284. successfully_executed_in_all_strategies,
  285. )
  286. def _accept_expression_in_executed_query(
  287. self, expression: Expression, successfully_executed_in_all_strategies: bool
  288. ) -> None:
  289. if not isinstance(expression, ExpressionWithArgs):
  290. return
  291. operation_variant = DbOperationVariant(
  292. expression.operation, expression.count_args()
  293. )
  294. stats = self.stats_by_operation_variant.get(operation_variant)
  295. assert (
  296. stats is not None
  297. ), f"no stats for {operation_variant.to_description()} found"
  298. stats.count_included_in_executed_queries = (
  299. stats.count_included_in_executed_queries + 1
  300. )
  301. if successfully_executed_in_all_strategies:
  302. stats.count_included_in_successfully_executed_queries = (
  303. stats.count_included_in_successfully_executed_queries + 1
  304. )
  305. for arg in expression.args:
  306. self._accept_expression_in_executed_query(
  307. arg, successfully_executed_in_all_strategies
  308. )