Skip to content

Commit 70c8e1a

Browse files
yaauiemashhurs
andauthored
tests(health api): extract specific assertions from list validator (#18937)
* tests(health api): extract specific assertions from list validator * backout no-op introspection changes * tests: add grok-lite $matcher for health api tests * back out unreferenced new functions * Apply suggestion from @mashhurs Co-authored-by: Mashhur <99575341+mashhurs@users.noreply.github.com> --------- Co-authored-by: Mashhur <99575341+mashhurs@users.noreply.github.com>
1 parent eb3756b commit 70c8e1a

8 files changed

Lines changed: 69 additions & 29 deletions

File tree

.buildkite/scripts/health-report-tests/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self) -> None:
2424
logstash_branch = os.environ.get("LS_BRANCH")
2525
if logstash_branch is None:
2626
# version is not specified, use the main branch, no need to git checkout
27-
print(f"LS_BRANCH is not specified, using main branch.")
27+
print(f"LS_BRANCH is not specified, using HEAD.")
2828
else:
2929
# LS_BRANCH accepts major latest as a major.x or specific branch as X.Y
3030
if logstash_branch.find(".x") == -1:

.buildkite/scripts/health-report-tests/scenario_executor.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,56 @@
22
A class to execute the given scenario for Logstash Health Report integration test
33
"""
44
import time
5+
import re
6+
from typing import Any
7+
from types import MappingProxyType
58
from logstash_health_report import LogstashHealthReport
69

710

811
class ScenarioExecutor:
912
logstash_health_report_api = LogstashHealthReport()
1013

1114
def __init__(self):
15+
self.matcher = self.GrokLite()
1216
pass
1317

14-
def __has_intersection(self, expects, results):
15-
# TODO: this logic is aligned on current Health API response
16-
# there is no guarantee that method correctly runs if provided multi expects and results
17-
# we expect expects to be existing in results
18-
for expect in expects:
19-
for result in results:
20-
if result.get('help_url') and "health-report-pipeline-" not in result.get('help_url'):
21-
return False
22-
if not all(key in result and result[key] == value for key, value in expect.items()):
23-
return False
24-
return True
18+
def __get_difference(self, expect: Any, actual: Any, path: str | None = None) -> list:
2519

26-
def __get_difference(self, differences: list, expectations: dict, reports: dict) -> dict:
27-
for key in expectations.keys():
20+
path = path or ""
21+
differences = []
2822

29-
if type(expectations.get(key)) != type(reports.get(key)):
30-
differences.append(f"Scenario expectation and Health API report structure differs for {key}.")
31-
return differences
23+
match expect:
24+
# $include is a substring matcher
25+
case {"$include": inclusion} if isinstance(expect, dict) and len(expect) == 1 and isinstance(actual, str):
26+
if inclusion not in actual:
27+
differences.append(f"Value at path `{path}` does not include:`{inclusion}`; got:`{actual}`")
28+
# $match is a grok-like matcher that anchors the pattern at both ends
29+
case {"$match": pattern_spec} if isinstance(expect, dict) and len(expect) == 1 and isinstance(actual, str):
30+
if not self.matcher.is_match(pattern_spec, actual):
31+
differences.append(f"Value at path `{path}` does not match pattern `{pattern_spec}`; got:`{actual}`")
32+
case dict():
33+
if not isinstance(actual, dict):
34+
differences.append(f"Structure differs at `{path}`, expected:`{expect}` got:`{actual}`")
35+
else:
36+
for key in expect.keys():
37+
differences.extend(self.__get_difference(expect.get(key), actual.get(key), f"{path}.{key}"))
38+
case list():
39+
if not isinstance(actual, list):
40+
differences.append(f"Structure differs at `{path}`, expected:`{expect}` got:`{actual}`")
41+
else:
42+
for index, (expectEntry, actualEntry) in enumerate(zip(expect, actual)):
43+
differences.extend(self.__get_difference(expectEntry, actualEntry, f"{path}[{index}]"))
44+
if len(actual) < len(expect):
45+
differences.append(f"Missing entries at path `{path}`, expected:`{len(expect)}`, got:`{len(actual)}`")
46+
case _:
47+
if expect != actual:
48+
differences.append(f"Value not match at path `{path}`; expected:`{expect}`, got:`{actual}`")
3249

33-
if isinstance(expectations.get(key), str):
34-
if expectations.get(key) != reports.get(key):
35-
differences.append({key: {"expected": expectations.get(key), "got": reports.get(key)}})
36-
continue
37-
elif isinstance(expectations.get(key), dict):
38-
self.__get_difference(differences, expectations.get(key), reports.get(key))
39-
elif isinstance(expectations.get(key), list):
40-
if not self.__has_intersection(expectations.get(key), reports.get(key)):
41-
differences.append({key: {"expected": expectations.get(key), "got": reports.get(key)}})
4250
return differences
4351

44-
def __is_expected(self, expectations: dict) -> None:
52+
def __is_expected(self, expectations: dict) -> bool:
4553
reports = self.logstash_health_report_api.get()
46-
differences = self.__get_difference([], expectations, reports)
54+
differences = self.__get_difference(expect=expectations, actual=reports)
4755
if differences:
4856
print("Differences found in 'expectation' section between YAML content and stats:")
4957
for diff in differences:
@@ -65,3 +73,27 @@ def on(self, scenario_name: str, expectations: dict) -> None:
6573
raise Exception(f"{scenario_name} failed.")
6674
else:
6775
print(f"Scenario `{scenario_name}` expectation meets the health report stats.")
76+
77+
78+
# GrokLite is a *LITE* implementation of Grok.
79+
# The idea is to allow you to use named patterns inside of regular expressions.
80+
# It does NOT support named captures, and mapping definitions CANNOT reference named patterns.
81+
class GrokLite:
82+
MAPPINGS = MappingProxyType({
83+
"ISO8601" : "[0-9]{4}-(?:0[0-9]|1[12])-(?:[0-2][0-9]|3[01])T(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:[.][0-9]+)?(?:Z|[+-](?:2[0-3]|[01][0-9])(?::?[0-5][0-9])?)",
84+
})
85+
86+
def __init__(self):
87+
self.pattern_cache = {}
88+
pass
89+
90+
def is_match(self, pattern_spec: str, value: str) -> bool:
91+
pattern = self.pattern_cache.get(pattern_spec)
92+
if pattern is None:
93+
replaced = re.sub(r"[{]([A-Z0-9_]+)[}]",
94+
lambda match: (self.MAPPINGS.get(match.group(1)) or match.group(0)),
95+
pattern_spec)
96+
pattern = re.compile(replaced)
97+
self.pattern_cache[pattern_spec] = pattern
98+
99+
return bool(re.search(pattern, value))

.buildkite/scripts/health-report-tests/tests/abnormal-termination.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ expectation:
2424
diagnosis:
2525
- cause: "pipeline is not running, likely because it has encountered an error"
2626
action: "view logs to determine the cause of abnormal pipeline shutdown"
27+
help_url: { $include: "health-report-pipeline-" }
2728
impacts:
2829
- description: "the pipeline is not currently processing"
2930
impact_areas: ["pipeline_execution"]

.buildkite/scripts/health-report-tests/tests/backpressure-1m.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ expectation:
2525
- id: "logstash:health:pipeline:flow:worker_utilization:diagnosis:1m-blocked"
2626
cause: "pipeline workers have been completely blocked for at least one minute"
2727
action: "address bottleneck or add resources"
28+
help_url: { $include: "health-report-pipeline-" }
2829
impacts:
2930
- id: "logstash:health:pipeline:flow:impact:blocked_processing"
3031
severity: 2

.buildkite/scripts/health-report-tests/tests/backpressure-5m.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ config:
99
pipeline.batch.size: 1
1010
conditions:
1111
full_start_required: true
12-
wait_seconds: 310 # give more seconds to make sure time is over the threshold, 1m in this case
12+
wait_seconds: 315 # give more seconds to make sure time is over the threshold, 5m in this case
1313
expectation:
1414
status: "red"
1515
symptom: "1 indicator is unhealthy (`pipelines`)"
@@ -25,6 +25,7 @@ expectation:
2525
- id: "logstash:health:pipeline:flow:worker_utilization:diagnosis:5m-blocked"
2626
cause: "pipeline workers have been completely blocked for at least five minutes"
2727
action: "address bottleneck or add resources"
28+
help_url: { $include: "health-report-pipeline-" }
2829
impacts:
2930
- id: "logstash:health:pipeline:flow:impact:blocked_processing"
3031
severity: 1

.buildkite/scripts/health-report-tests/tests/multipipeline.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ expectation:
3737
diagnosis:
3838
- cause: "pipeline is loading"
3939
action: "if pipeline does not come up quickly, you may need to check the logs to see if it is stalled"
40+
help_url: { $include: "health-report-pipeline-" }
4041
impacts:
4142
- impact_areas: ["pipeline_execution"]
4243
details:
@@ -48,6 +49,7 @@ expectation:
4849
diagnosis:
4950
- cause: "pipeline has finished running because its inputs have been closed and events have been processed"
5051
action: "if you expect this pipeline to run indefinitely, you will need to configure its inputs to continue receiving or fetching events"
52+
help_url: { $include: "health-report-pipeline-" }
5153
impacts:
5254
- impact_areas: [ "pipeline_execution" ]
5355
details:
@@ -59,6 +61,7 @@ expectation:
5961
diagnosis:
6062
- cause: "pipeline is not running, likely because it has encountered an error"
6163
action: "view logs to determine the cause of abnormal pipeline shutdown"
64+
help_url: { $include: "health-report-pipeline-" }
6265
impacts:
6366
- description: "the pipeline is not currently processing"
6467
impact_areas: [ "pipeline_execution" ]

.buildkite/scripts/health-report-tests/tests/normal-termination.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ expectation:
2323
diagnosis:
2424
- cause: "pipeline has finished running because its inputs have been closed and events have been processed"
2525
action: "if you expect this pipeline to run indefinitely, you will need to configure its inputs to continue receiving or fetching events"
26+
help_url: { $include: "health-report-pipeline-" }
2627
impacts:
2728
- impact_areas: ["pipeline_execution"]
2829
details:

.buildkite/scripts/health-report-tests/tests/slow-start.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ expectation:
2424
diagnosis:
2525
- cause: "pipeline is loading"
2626
action: "if pipeline does not come up quickly, you may need to check the logs to see if it is stalled"
27+
help_url: { $include: "health-report-pipeline-" }
2728
impacts:
2829
- impact_areas: ["pipeline_execution"]
2930
details:

0 commit comments

Comments
 (0)