Skip to content

Commit

Permalink
requires/types: introduce expression type (expr.py)
Browse files Browse the repository at this point in the history
`expression` is a new requirement type that allows user to express a requirement
in human-readable, SQL-like syntax. It has a proper grammar to allow user to write
checks as easily digestable manner, which will improve the QOL for scenario writers
and code reviewers. The grammar is constructed using `pyparsing` library.

The current grammar supports the following constructs:

- Keywords (True, False, None)
- Constants (Integer, Float, String literal)
- Runtime variables (Python properties)
- Functions(len, not, file, systemd, read_ini, read_cert)
- Arithmetic operators(sign, mul, div, add, sub, exp)
- Comparison operators(<, <=, >, >=, ==, !=, in)
- Logical operators(and, or, not)
- Comments('#', '//', '/*...*/')

Also, the following changes have been made:

- Updated requirement_types.rst to document the new Expression requirement type.
- Moved property resolver logic from YPropertyBase to PythonEntityResolver class.
- Added `pyparsing` as a dependency.
- Added unit tests for the new code.

This patch rewrites some of the scenario checks in the new expression syntax to
demonstrate the difference and provide examples.

Signed-off-by: Mustafa Kemal Gilor <[email protected]>
  • Loading branch information
xmkg committed Jul 4, 2024
1 parent 7844aac commit 79b363d
Show file tree
Hide file tree
Showing 26 changed files with 2,737 additions and 348 deletions.
619 changes: 619 additions & 0 deletions doc/source/contrib/language_ref/property_ref/requirement_types.rst

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions hotsos/core/plugins/kernel/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,13 @@ def huge_pages_enabled(self):

@property
def hugetlb_to_mem_total_percentage(self):
return round((self.Hugetlb * 100) / self.MemTotal)
return (self.MemTotal and round(
(self.Hugetlb * 100) / self.MemTotal)) or 0

@property
def mem_avail_to_mem_total_percentage(self):
return round((self.MemAvailable * 100) / self.MemTotal)
return (self.MemTotal and round(
(self.MemAvailable * 100) / self.MemTotal)) or 0

@property
def hugep_used_to_hugep_total_percentage(self):
Expand Down
14 changes: 13 additions & 1 deletion hotsos/core/ycheck/engine/properties/checks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from functools import cached_property

from propertree.propertree2 import PTreeLogicalGrouping
from propertree.propertree2 import (
PTreeLogicalGrouping,
PTreeOverrideLiteralType
)
from hotsos.core.log import log
from hotsos.core.ycheck.engine.properties.common import (
YPropertyOverrideBase,
Expand All @@ -11,6 +14,9 @@
from hotsos.core.ycheck.engine.properties.requires.requires import (
YPropertyRequires
)
from hotsos.core.ycheck.engine.properties.requires.types.expr import (
YPropertyExpr
)
from hotsos.core.ycheck.engine.properties.search import (
YPropertySearch,
)
Expand Down Expand Up @@ -146,6 +152,12 @@ def result(self):
stop_executon = False
for member in self.members:
for item in member:
# Allow implicit declaration of expression.
if isinstance(item, PTreeOverrideLiteralType):
item = YPropertyExpr(item.root, "expression",
item.content, item._override_path,
context=self.context)

# Ignore these here as they are used by search properties.
if isinstance(item, YPropertyInput):
continue
Expand Down
97 changes: 52 additions & 45 deletions hotsos/core/ycheck/engine/properties/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,52 +266,28 @@ def __getattr__(self, key):
return self.data[key]


class YPropertyBase(PTreeOverrideBase):
class PythonEntityResolver:
"""A class to resolve Python entities (e.g. variable, property)
by their import path."""

def __init__(self, *args, **kwargs):
self._cache = PropertyCache()
super().__init__(*args, **kwargs)

def resolve_var(self, name):
"""
Resolve variable with name to value. This can be used speculatively and
will return the name as value if it can't be resolved.
"""
if not name.startswith('$'):
return name

if hasattr(self, 'context'):
if self.context.vars:
_name = name.partition('$')[2]
return self.context.vars.resolve(_name)
# Class-level cache for Python module imports for future use
import_cache = None

log.warning("could not resolve var '%s' - vars not found in "
"context", name)

return name

@property
def cache(self):
"""
All properties get their own cache object that they can use as they
wish.
"""
return self._cache
def __init__(self, context):
self.context = context

def _load_from_import_cache(self, key):
""" Retrieve from global context if one exists.
""" Retrieve from global cache if one exists.
@param key: key to retrieve
"""
if self.context is None:
log.info("context not available - cannot load '%s'", key)

if not self.import_cache:
log.info("cannot load import `%s` from cache,"
" import cache is not initialized yet", key)
return

# we save all imports in a dict called "import_cache" within the
# global context so that all properties have access.
c = getattr(self.context, 'import_cache')
if c:
return c.get(key)
return self.import_cache.get(key)

@staticmethod
def _get_mod_class_from_path(path):
Expand Down Expand Up @@ -350,16 +326,14 @@ def _add_to_import_cache(self, key, value):
@param key: key to save
@param value: value to save
"""
if self.context is None:
log.info("context not available - cannot save '%s'", key)

if not self.import_cache:
self.import_cache = {key: value}
log.debug("import cache initialized with initial key `%s`", key)
return

c = getattr(self.context, 'import_cache')
if c:
c[key] = value
else:
c = {key: value}
setattr(self.context, 'import_cache', c)
self.import_cache[key] = value
log.debug("import cache updated for key %s", key)

def get_cls(self, import_str):
""" Import and instantiate Python class.
Expand Down Expand Up @@ -511,6 +485,39 @@ def get_import(self, import_str):
return self.get_attribute(import_str)


class YPropertyBase(PTreeOverrideBase, PythonEntityResolver):

def __init__(self, *args, **kwargs):
self._cache = PropertyCache()
super().__init__(*args, **kwargs)

def resolve_var(self, name):
"""
Resolve variable with name to value. This can be used speculatively and
will return the name as value if it can't be resolved.
"""
if not name.startswith('$'):
return name

if hasattr(self, 'context'):
if self.context.vars:
_name = name.partition('$')[2]
return self.context.vars.resolve(_name)

log.warning("could not resolve var '%s' - vars not found in "
"context", name)

return name

@property
def cache(self):
"""
All properties get their own cache object that they can use as they
wish.
"""
return self._cache


class YPropertyOverrideBase(YPropertyBase, PTreeOverrideBase):
pass

Expand Down
4 changes: 3 additions & 1 deletion hotsos/core/ycheck/engine/properties/requires/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
property as rproperty,
path,
varops,
expr,
)

CACHE_CHECK_KEY = '__PREVIOUSLY_CACHED_PROPERTY_TYPE'
Expand Down Expand Up @@ -78,7 +79,8 @@ class YPropertyRequires(YPropertyMappedOverrideBase):
systemd.YRequirementTypeSystemd,
rproperty.YRequirementTypeProperty,
path.YRequirementTypePath,
varops.YPropertyVarOps]
varops.YPropertyVarOps,
expr.YPropertyExpr,]
# We want to be able to use this property both on its own and as a member
# of other mapping properties e.g. Checks. The following setting enables
# this.
Expand Down
Loading

0 comments on commit 79b363d

Please sign in to comment.