22A class to execute the given scenario for Logstash Health Report integration test
33"""
44import time
5+ import re
6+ from typing import Any
7+ from types import MappingProxyType
58from logstash_health_report import LogstashHealthReport
69
710
811class 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 ))
0 commit comments