def test_large_durations(self): max_duration = 999999999 * 24 * 60 * 60 * 1000 assert parse_duration(999999999, "d") == max_duration assert parse_duration(999999999 * 24, "h") == max_duration assert parse_duration(999999999 * 24 * 60, "m") == max_duration assert parse_duration(999999999 * 24 * 60 * 60, "s") == max_duration assert parse_duration(999999999 * 24 * 60 * 60 * 1000, "ms") == max_duration
def test_overflow_durations(self): with self.assertRaises(InvalidQuery): assert parse_duration(999999999 + 1, "d") with self.assertRaises(InvalidQuery): assert parse_duration((999999999 + 1) * 24, "h") with self.assertRaises(InvalidQuery): assert parse_duration((999999999 + 1) * 24 * 60 + 1, "m") with self.assertRaises(InvalidQuery): assert parse_duration((999999999 + 1) * 24 * 60 * 60 + 1, "s") with self.assertRaises(InvalidQuery): assert parse_duration((999999999 + 1) * 24 * 60 * 60 * 1000 + 1, "ms")
def visit_aggregate_filter(self, node, children): (negation, search_key, _, operator, search_value) = children operator = self.handle_negation(negation, operator) search_value = search_value[0] if not isinstance(search_value, RegexNode) else search_value try: aggregate_value = None if search_value.expr_name in ["duration_format", "percentage_format"]: # Even if the search value matches duration format, only act as duration for certain columns function = resolve_field( search_key.name, self.params, functions_acl=FUNCTIONS.keys() ) if function.aggregate is not None: if search_value.expr_name == "percentage_format" and self.is_percentage_key( function.aggregate[0] ): aggregate_value = parse_percentage(*search_value.match.groups()) # Extract column and function name out so we can check if we should parse as duration elif search_value.expr_name == "duration_format" and self.is_duration_key( function.aggregate[1] ): aggregate_value = parse_duration(*search_value.match.groups()) if aggregate_value is None: aggregate_value = parse_numeric_value(*search_value.match.groups()) except ValueError: raise InvalidSearchQuery(f"Invalid aggregate query condition: {search_key}") except InvalidQuery as exc: raise InvalidSearchQuery(str(exc)) return AggregateFilter(search_key, operator, SearchValue(aggregate_value))
def visit_aggregate_duration_filter(self, node, children): (negation, search_key, _, operator, search_value) = children operator = handle_negation(negation, operator) try: # Even if the search value matches duration format, only act as # duration for certain columns function = resolve_field(search_key.name, self.params, functions_acl=FUNCTIONS.keys()) is_duration_key = False if function.aggregate is not None: args = function.aggregate[1] if isinstance(args, list): is_duration_key = all(self.is_duration_key(arg) for arg in args) else: is_duration_key = self.is_duration_key(args) if is_duration_key: aggregate_value = parse_duration(*search_value) else: # Duration overlaps with numeric values with `m` (million vs # minutes). So we fall through to numeric if it's not a # duration key # # TODO(epurkhiser): Should we validate that the field is # numeric and do some other fallback if it's not? aggregate_value = parse_numeric_value(*search_value) except ValueError: raise InvalidSearchQuery(f"Invalid aggregate query condition: {search_key}") except InvalidQuery as exc: raise InvalidSearchQuery(str(exc)) return AggregateFilter(search_key, operator, SearchValue(aggregate_value))
def visit_duration_filter(self, node, children): (search_key, sep, operator, search_value) = children operator = operator[0] if not isinstance(operator, Node) else "=" if self.is_duration_key(search_key.name): try: search_value = parse_duration(*search_value.match.groups()) except InvalidQuery as exc: raise InvalidSearchQuery(str(exc)) return SearchFilter(search_key, operator, SearchValue(search_value)) elif self.is_numeric_key(search_key.name): return self.visit_numeric_filter(node, (search_key, sep, ((operator, search_value),))) else: search_value = operator + search_value.text if operator != "=" else search_value.text return self._handle_basic_filter(search_key, "=", SearchValue(search_value))
def visit_duration_filter(self, node, children): (search_key, sep, operator, search_value) = children operator = operator[0] if not isinstance(operator, Node) else "=" if self.is_duration_key(search_key.name): try: search_value = parse_duration(*search_value) except InvalidQuery as exc: raise InvalidSearchQuery(str(exc)) return SearchFilter(search_key, operator, SearchValue(search_value)) # Durations overlap with numeric `m` suffixes if self.is_numeric_key(search_key.name): return self._handle_numeric_filter(search_key, operator, search_value) search_value = "".join(search_value) search_value = operator + search_value if operator != "=" else search_value return self._handle_basic_filter(search_key, "=", SearchValue(search_value))
def visit_duration_filter(self, node, children): (negation, search_key, _, operator, search_value) = children if self.is_duration_key(search_key.name) or self.is_numeric_key(search_key.name): operator = handle_negation(negation, operator) else: operator = get_operator_value(operator) if self.is_duration_key(search_key.name): try: search_value = parse_duration(*search_value) except InvalidQuery as exc: raise InvalidSearchQuery(str(exc)) return SearchFilter(search_key, operator, SearchValue(search_value)) # Durations overlap with numeric `m` suffixes if self.is_numeric_key(search_key.name): return self._handle_numeric_filter(search_key, operator, search_value) search_value = "".join(search_value) search_value = operator + search_value if operator not in ("=", "!=") else search_value operator = "!=" if is_negated(negation) else "=" return self._handle_basic_filter(search_key, operator, SearchValue(search_value))
def node_visitor(token): if token["type"] == "spaces": return None if token["type"] == "filter": # Filters with an invalid reason raises to signal to the test # runner that we should expect this exception if token.get("invalid"): raise InvalidSearchQuery(token["invalid"]["reason"]) # Transform the operator to match for list values if token["value"]["type"] in ["valueTextList", "valueNumberList"]: operator = "NOT IN" if token["negated"] else "IN" else: # Negate the operator if the filter is negated to match operator = token["operator"] or "=" operator = f"!{operator}" if token["negated"] else operator key = node_visitor(token["key"]) value = node_visitor(token["value"]) if token["filter"] == "boolean" and token["negated"]: operator = "=" value = SearchValue(raw_value=1 if value.raw_value == 0 else 0) return SearchFilter(key, operator, value) if token["type"] == "keySimple": return SearchKey(name=token["value"]) if token["type"] == "keyExplicitTag": return SearchKey(name=f"tags[{token['key']['value']}]") if token["type"] == "keyAggregate": name = node_visitor(token["name"]).name # Consistent join aggregate function parameters args = ", ".join(arg["value"]["value"] for arg in token["args"]["args"]) return AggregateKey(name=f"{name}({args})") if token["type"] == "valueText": # Noramlize values by removing the escaped quotes value = token["value"].replace('\\"', '"') return SearchValue(raw_value=value) if token["type"] == "valueNumber": return SearchValue( raw_value=parse_numeric_value(token["value"], token["unit"])) if token["type"] == "valueTextList": return SearchValue( raw_value=[item["value"]["value"] for item in token["items"]]) if token["type"] == "valueNumberList": return SearchValue(raw_value=[ item["value"]["rawValue"] for item in token["items"] ]) if token["type"] == "valueIso8601Date": return SearchValue(raw_value=parse_datetime_string(token["value"])) if token["type"] == "valueDuration": return SearchValue( raw_value=parse_duration(token["value"], token["unit"])) if token["type"] == "valueRelativeDate": return SearchValue( raw_value=parse_duration(token["value"], token["unit"])) if token["type"] == "valueBoolean": return SearchValue(raw_value=int(token["value"])) if token["type"] == "freeText": if token["quoted"]: # Normalize quotes value = token["value"].replace('\\"', '"') else: # Normalize spacing value = token["value"].strip(" ") if value == "": return None return SearchFilter( key=SearchKey(name="message"), operator="=", value=SearchValue(raw_value=value), )
def test_errors(self): with self.assertRaises(InvalidQuery): parse_duration("test", "ms") with self.assertRaises(InvalidQuery): parse_duration(123, "test")
def test_weeks(self): assert parse_duration(890, "wk") == 890 * 7 * 24 * 60 * 60 * 1000 assert parse_duration(890, "w") == 890 * 7 * 24 * 60 * 60 * 1000
def test_days(self): assert parse_duration(567, "day") == 567 * 24 * 60 * 60 * 1000 assert parse_duration(567, "d") == 567 * 24 * 60 * 60 * 1000
def test_hours(self): assert parse_duration(234, "hr") == 234 * 60 * 60 * 1000 assert parse_duration(234, "h") == 234 * 60 * 60 * 1000
def test_minutes(self): assert parse_duration(789, "min") == 789 * 60 * 1000 assert parse_duration(789, "m") == 789 * 60 * 1000
def test_sec(self): assert parse_duration(456, "s") == 456000
def test_ms(self): assert parse_duration(123, "ms") == 123