-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
774d06c
commit b07427a
Showing
4 changed files
with
307 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
scripts/py_matter_yamltests/matter_yamltests/pics_checker.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |