Skip to content

Commit

Permalink
[matter_yamltests] Add PICS checker
Browse files Browse the repository at this point in the history
  • Loading branch information
vivien-apple committed Jan 19, 2023
1 parent 774d06c commit b07427a
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 20 deletions.
6 changes: 5 additions & 1 deletion scripts/py_matter_yamltests/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ pw_python_package("matter_yamltests") {
"matter_yamltests/definitions.py",
"matter_yamltests/fixes.py",
"matter_yamltests/parser.py",
"matter_yamltests/pics_checker.py",
]

python_deps = [ "${chip_root}/scripts/py_matter_idl:matter_idl" ]

tests = [ "test_spec_definitions.py" ]
tests = [
"test_spec_definitions.py",
"test_pics_checker.py",
]

# TODO: at a future time consider enabling all (* or missing) here to get
# pylint checking these files
Expand Down
51 changes: 32 additions & 19 deletions scripts/py_matter_yamltests/matter_yamltests/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
import yaml

from . import fixes
from .definitions import SpecDefinitions
from .constraints import get_constraints
from .pics_checker import PICSChecker


_TESTS_SECTION = [
'name',
Expand Down Expand Up @@ -183,7 +186,7 @@ class _TestStepWithPlaceholders:
processed.
'''

def __init__(self, test: dict, config: dict, definitions):
def __init__(self, test: dict, config: dict, definitions: SpecDefinitions, pics_checker: PICSChecker):
# Disabled tests are not parsed in order to allow the test to be added to the test
# suite even if the feature is not implemented yet.
self.is_enabled = not ('disabled' in test and test['disabled'])
Expand All @@ -201,6 +204,7 @@ def __init__(self, test: dict, config: dict, definitions):
self.command = _value_or_config(test, 'command', config)
self.attribute = _value_or_none(test, 'attribute')
self.endpoint = _value_or_config(test, 'endpoint', config)
self.is_pics_enabled = pics_checker.check(_value_or_none(test, 'PICS'))

self.identity = _value_or_none(test, 'identity')
self.fabric_filtered = _value_or_none(test, 'fabricFiltered')
Expand Down Expand Up @@ -376,6 +380,10 @@ def __init__(self, test: _TestStepWithPlaceholders, runtime_config_variable_stor
def is_enabled(self):
return self._test.is_enabled

@property
def is_pics_enabled(self):
return self._test.is_pics_enabled

@property
def is_attribute(self):
return self._test.is_attribute
Expand Down Expand Up @@ -677,8 +685,9 @@ def _config_variable_substitution(self, value):
variable_info = self._runtime_config_variable_storage[token]
if type(variable_info) is dict and 'defaultValue' in variable_info:
variable_info = variable_info['defaultValue']
tokens[idx] = variable_info
substitution_occured = True
if variable_info is not None:
tokens[idx] = variable_info
substitution_occured = True

if len(tokens) == 1:
return tokens[0]
Expand All @@ -703,12 +712,12 @@ class YamlTests:
multiple runs.
'''

def __init__(self, parsing_config_variable_storage: dict, definitions, tests: dict):
def __init__(self, parsing_config_variable_storage: dict, definitions: SpecDefinitions, pics_checker: PICSChecker, tests: dict):
self._parsing_config_variable_storage = parsing_config_variable_storage
enabled_tests = []
for test in tests:
test_with_placeholders = _TestStepWithPlaceholders(
test, self._parsing_config_variable_storage, definitions)
test, self._parsing_config_variable_storage, definitions, pics_checker)
if test_with_placeholders.is_enabled:
enabled_tests.append(test_with_placeholders)
fixes.try_update_yaml_node_id_test_runner_state(
Expand All @@ -735,24 +744,28 @@ def __next__(self) -> TestStep:

class TestParser:
def __init__(self, test_file, pics_file, definitions):
# TODO Needs supports for PICS file
with open(test_file) as f:
loader = yaml.FullLoader
loader = fixes.try_add_yaml_support_for_scientific_notation_without_dot(
loader)
data = self.__load_yaml(test_file)

data = yaml.load(f, Loader=loader)
_check_valid_keys(data, _TESTS_SECTION)
_check_valid_keys(data, _TESTS_SECTION)

self.name = _value_or_none(data, 'name')
self.PICS = _value_or_none(data, 'PICS')
self.name = _value_or_none(data, 'name')
self.PICS = _value_or_none(data, 'PICS')

self._parsing_config_variable_storage = _value_or_none(
data, 'config')
self._parsing_config_variable_storage = _value_or_none(data, 'config')

tests = _value_or_none(data, 'tests')
self.tests = YamlTests(
self._parsing_config_variable_storage, definitions, tests)
pics_checker = PICSChecker(pics_file)
tests = _value_or_none(data, 'tests')
self.tests = YamlTests(
self._parsing_config_variable_storage, definitions, pics_checker, tests)

def update_config(self, key, value):
self._parsing_config_variable_storage[key] = value

def __load_yaml(self, test_file):
with open(test_file) as f:
loader = yaml.FullLoader
loader = fixes.try_add_yaml_support_for_scientific_notation_without_dot(
loader)

return yaml.load(f, Loader=loader)
return None
139 changes: 139 additions & 0 deletions scripts/py_matter_yamltests/matter_yamltests/pics_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#
# Copyright (c) 2023 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unicodedata

COMMENT_CHARACTER = '#'
VALUE_SEPARATOR = '='
# The only correct value to enable a feature is '1'. All others turns it off.
VALUE_ENABLED = '1'


class PICSChecker():
__pics: None
__index: 0

def __init__(self, pics_file: str):
if pics_file is not None:
self.__pics = self.__parse(pics_file)

def check(self, pics):
if pics is None:
return True

self.__index = 0
tokens = self.__tokenize(pics)
return self.__evaluate_expression(tokens, self.__pics)

def __parse(self, pics_file: str):
pics = {}
with open(pics_file) as f:
line = f.readline()
while line:
line = self.__clear_input(line)
if len(line) and line[0] != COMMENT_CHARACTER:
key, value = line.split(VALUE_SEPARATOR)
pics[key] = value == VALUE_ENABLED
line = f.readline()
return pics

def __evaluate_expression(self, tokens: list[str], pics: dict):
leftExpr = self.__evaluate_sub_expression(tokens, pics)
if self.__index >= len(tokens):
return leftExpr

if tokens[self.__index] == ')':
return leftExpr

token = tokens[self.__index]

if token == '&&':
self.__index += 1
rightExpr = self.__evaluate_sub_expression(tokens, pics)
return leftExpr and rightExpr

if token == '||':
self.__index += 1
rightExpr = self.__evaluate_sub_expression(tokens, pics)
return leftExpr or rightExpr

raise KeyError(f'Unknown token: {token}')

def __evaluate_sub_expression(self, tokens: list[str], pics: dict):
token = tokens[self.__index]
if token == '(':
self.__index += 1
expr = self.__evaluate_expression(tokens, pics)
if tokens[self.__index] != ')':
raise KeyError('Missing ")"')

self.__index += 1
return expr

if token == '!':
self.__index += 1
expr = self.__evaluate_expression(tokens, pics)
return not expr

token = self.__normalize(token)
self.__index += 1

if pics.get(token) == None:
# By default, let's consider that if a PICS item is not defined, it is |false|.
# It allows to create a file that only contains enabled features.
return False

return pics.get(token)

def __tokenize(self, expression: str):
token = ''
tokens = []

for c in expression:
if c == ' ' or c == '\n' or c == '\n':
pass
elif c == '(' or c == ')' or c == '!':
if len(token):
tokens.append(token)
token = ''
tokens.append(c)
elif c == '&' or c == '|':
if len(token) and token[-1] == c:
token = token[:-1]
if len(token):
tokens.append(token)
token = ''
tokens.append(c + c)
else:
token += c
else:
token += c

if len(token):
tokens.append(token)
token = ''

return tokens

def __clear_input(self, value: str):
return ''.join(c for c in value if unicodedata.category(c)[0] != 'C').replace(' ', '').lower()

def __normalize(self, token: str):
# Convert to all-lowercase so people who mess up cases don't have things
# break on them in subtle ways.
token = token.lower()

# TODO strip off "(Additional Context)" bits from the end of the code.
return token
131 changes: 131 additions & 0 deletions scripts/py_matter_yamltests/test_pics_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env -S python3 -B
#
# Copyright (c) 2022 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from matter_yamltests.pics_checker import PICSChecker

import unittest
import io

from unittest.mock import mock_open, patch

empty_config = ''

simple_config = '''
A.A=0
A.B=1
A.C=0
'''

simple_config_with_mistakes = '''
A.A=FOO
A.B=2
'''

simple_config_with_comments = '''
# This is a comment
A.A=0
# This is an other comment
A.B=1
'''

simple_config_with_whitespaces_and_control_characters = '''
A.A=0 \n
\tA.B = 1
'''


class TestSpecDefinitions(unittest.TestCase):
@patch('builtins.open', mock_open(read_data=empty_config))
def test_empty_config(self):
pics_checker = PICSChecker('')
self.assertIsInstance(pics_checker, PICSChecker)

@patch('builtins.open', mock_open(read_data=simple_config))
def test_simple_config(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A'))
self.assertFalse(pics_checker.check('A.DoesNotExist'))

self.assertTrue(pics_checker.check('A.B'))
self.assertTrue(pics_checker.check('A.b'))
self.assertTrue(pics_checker.check('a.b'))
self.assertTrue(pics_checker.check(' A.B'))
self.assertTrue(pics_checker.check('A.B '))

@patch('builtins.open', mock_open(read_data=simple_config))
def test_logic_negation(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.C'))
self.assertTrue(pics_checker.check('!A.C'))
self.assertFalse(pics_checker.check('!!A.C'))
self.assertTrue(pics_checker.check('!!!A.C'))

@patch('builtins.open', mock_open(read_data=simple_config))
def test_logical_and(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.C && A.B'))
self.assertFalse(pics_checker.check('A.C && A.B'))
self.assertTrue(pics_checker.check('!A.A && A.B'))

@patch('builtins.open', mock_open(read_data=simple_config))
def test_logical_or(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A || A.C'))
self.assertTrue(pics_checker.check('A.B || A.C'))
self.assertTrue(pics_checker.check('!A.A || A.C'))
self.assertTrue(pics_checker.check('A.A || A.B || A.C'))

@patch('builtins.open', mock_open(read_data=simple_config))
def test_logical_parenthesis(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('(A.A)'))
self.assertTrue(pics_checker.check('(A.B)'))
self.assertTrue(pics_checker.check('!(A.A)'))
self.assertTrue(pics_checker.check('(!(A.A))'))
self.assertFalse(pics_checker.check('(A.A && A.B)'))
self.assertFalse(pics_checker.check('((A.A) && (A.B))'))
self.assertTrue(pics_checker.check('(!A.A && A.B)'))
self.assertTrue(pics_checker.check('(!(A.A) && (A.B))'))
self.assertTrue(pics_checker.check('(A.A || A.B)'))
self.assertFalse(pics_checker.check('(A.A || !A.B)'))

@patch('builtins.open', mock_open(read_data=simple_config_with_mistakes))
def test_simple_config_with_mistakes(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A'))
self.assertFalse(pics_checker.check('A.B'))

@patch('builtins.open', mock_open(read_data=simple_config_with_comments))
def test_simple_config_with_comments(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A'))
self.assertTrue(pics_checker.check('A.B'))

@patch('builtins.open', mock_open(read_data=simple_config_with_whitespaces_and_control_characters))
def test_simple_config_with_comments(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A'))
self.assertTrue(pics_checker.check('A.B'))

@patch('builtins.open', mock_open(read_data=simple_config))
def test_simple_config_with_comments(self):
pics_checker = PICSChecker('')
self.assertFalse(pics_checker.check('A.A'))
self.assertTrue(pics_checker.check('A.B'))


if __name__ == '__main__':
unittest.main()

0 comments on commit b07427a

Please sign in to comment.