blob: c233bff116d5013d2ba8d648ca103052ec0e95ae [file]
#
# Copyright (c) 2022 Project CHIP Authors
# All rights reserved.
#
# 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.
#
# TODO Once yamltest is a proper self contained module we can move this file
# to a more appropriate spot. For now, having this file to do some quick checks
# is arguably better then no checks at all.
import io
import unittest
from unittest.mock import mock_open, patch
from matter.yamltests.definitions import ParseSource, SpecDefinitions
from matter.yamltests.errors import TestStepEnumError, TestStepEnumSpecifierNotUnknownError, TestStepEnumSpecifierWrongError
from matter.yamltests.parser import TestParser, TestParserConfig, build_revision_var_name
simple_test_description = '''<?xml version="1.0"?>
<configurator>
<enum name="TestEnum" type="enum8">
<cluster code="0x1234"/>
<item name="A" value="0x0"/>
<item name="B" value="0x1"/>
<item name="C" value="0x2"/>
</enum>
<struct name="TestStruct">
<cluster code="0x1234"/>
<item name="a" type="boolean"/>
</struct>
<cluster>
<name>Test</name>
<code>0x1234</code>
<attribute side="server" code="0x0024" type="TestEnum" writable="true" optional="false">test_enum</attribute>
<attribute side="server" code="0xFFFD" type="int16u" writable="false" optional="false">ClusterRevision</attribute>
<command source="client" code="1" name="test"></command>
<command source="client" code="2" name="IntTest">
<arg name="arg" type="int8u"/>
</command>
</cluster>
</configurator>
'''
simple_test_yaml = '''
name: Test Cluster Tests
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Send Test Command"
command: "test"
'''
enum_values_yaml = '''
name: Test Enum Values
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Read attribute test_enum Value"
command: "readAttribute"
attribute: "test_enum"
response:
value: 0
- label: "Read attribute test_enum Value"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.A
- label: "Read attribute test_enum Value"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.A(0)
- label: "Read attribute test_enum Value"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.UnknownEnumValue
- label: "Read attribute test_enum Value"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.UnknownEnumValue(255)
- label: "Write attribute test_enum Value"
command: "writeAttribute"
attribute: "test_enum"
arguments:
value: 0
- label: "Write attribute test_enum Value"
command: "writeAttribute"
attribute: "test_enum"
arguments:
value: TestEnum.A
- label: "Write attribute test_enum Value"
command: "writeAttribute"
attribute: "test_enum"
arguments:
value: TestEnum.A(0)
- label: "Write attribute test_enum Value"
command: "writeAttribute"
attribute: "test_enum"
arguments:
value: TestEnum.UnknownEnumValue
- label: "Write attribute test_enum Value"
command: "writeAttribute"
attribute: "test_enum"
arguments:
value: TestEnum.UnknownEnumValue(255)
'''
enum_value_read_response_wrong_code_yaml = '''
tests:
- label: "Read attribute test_enum Value"
cluster: "Test"
command: "readAttribute"
attribute: "test_enum"
response:
value: 123
'''
enum_value_read_response_wrong_name_yaml = '''
tests:
- label: "Read attribute test_enum Value"
cluster: "Test"
command: "readAttribute"
attribute: "test_enum"
response:
value: ThisIsWrong
'''
enum_value_read_response_wrong_code_specified_yaml = '''
tests:
- label: "Read attribute test_enum Value"
cluster: "Test"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.A(123)
'''
enum_value_read_response_not_unknown_code_specified_yaml = '''
tests:
- label: "Read attribute test_enum Value"
cluster: "Test"
command: "readAttribute"
attribute: "test_enum"
response:
value: TestEnum.UnknownEnumValue(0)
'''
_BASIC_ARITHMETIC_ARG_RESULTS = [6, 6, 2, 2, 8, 8, 2, 2, 1]
basic_arithmetic_yaml = '''
name: Test Cluster Tests
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
myVariable: 4
tests:
- label: "Add 2"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable + 2
- label: "Add 2 with no space"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable+2
- label: "Minus 2"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable - 2
- label: "Minus 2 with no space"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable-2
- label: "Multiply by 2"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable * 2
- label: "Multiply by 2 with no space"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable*2
- label: "Divide by 2"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable / 2
- label: "Divide by 2 with no space"
command: "IntTest"
arguments:
values:
- name: "arg"
value: myVariable/2
- label: "Arithmetic with parentheses"
command: "IntTest"
arguments:
values:
- name: "arg"
value: (myVariable +3)/7
'''
no_revision_check_yaml = '''
name: Test without any min/maxRevision applied
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Step 1 - No revision check"
command: "test"
- label: "Step 2 - No revision check"
command: "test"
- label: "Step 3 - No revision check"
command: "test"
'''
min_max_revision_injection_yaml = '''
name: Test Min/Max Revision Injection
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Step 1 - No revision check"
command: "test"
- label: "Step 2 - With revision check"
command: "test"
minRevision: 5
- label: "Step 3 - Also with revision check"
command: "test"
maxRevision: 10
'''
min_max_revision_check_yaml = '''
name: Test Min/Max Revision Check
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Step 1 - The revision check"
command: "test"
minRevision: 5
maxRevision: 10
'''
# A YAML to test ClusterRevision check injection on two different endpoints
min_max_revision_multi_injection_yaml = '''
name: Test Min/Max Revision Multi Injection
config:
nodeId: 0x12344321
cluster: "Test"
endpoint: 1
tests:
- label: "Step 1 - EP 1"
command: "test"
minRevision: 5
- label: "Step 2 - EP 2"
endpoint: 2
command: "test"
maxRevision: 10
- label: "Step 3 - EP 1 again"
command: "test"
maxRevision: 10
- label: "Step 4 - EP 2 again"
endpoint: 2
command: "test"
minRevision: 5
'''
def mock_open_with_parameter_content(content):
file_object = mock_open(read_data=content).return_value
file_object.__iter__.return_value = content.splitlines(True)
return file_object
@patch('builtins.open', new=mock_open_with_parameter_content)
class TestYamlParser(unittest.TestCase):
def setUp(self):
self._definitions = SpecDefinitions(
[ParseSource(source=io.StringIO(simple_test_description), name='simple_test_description')])
def test_able_to_iterate_over_all_parsed_tests(self):
# self._yaml_parser.tests implements `__next__`, which does value substitution. We are
# simply ensure there is no exceptions raise.
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(simple_test_yaml, parser_config)
count = 0
for idx, test_step in enumerate(yaml_parser.tests):
count += 1
pass
self.assertEqual(count, 1)
def test_config(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(simple_test_yaml, parser_config)
for idx, test_step in enumerate(yaml_parser.tests):
self.assertEqual(test_step.node_id, 0x12344321)
self.assertEqual(test_step.cluster, 'Test')
self.assertEqual(test_step.endpoint, 1)
def test_basic_arithmetic(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(basic_arithmetic_yaml, parser_config)
for idx, test_step in enumerate(yaml_parser.tests):
values = test_step.arguments['values']
self.assertEqual(len(values), 1)
value = values[0]
self.assertEqual(value['name'], 'arg')
self.assertEqual(
value['value'], _BASIC_ARITHMETIC_ARG_RESULTS[idx])
def test_config_override(self):
config_override = {'nodeId': 12345,
'cluster': 'TestOverride', 'endpoint': 4}
parser_config = TestParserConfig(
None, self._definitions, config_override)
yaml_parser = TestParser(simple_test_yaml, parser_config)
for idx, test_step in enumerate(yaml_parser.tests):
self.assertEqual(test_step.node_id, 12345)
self.assertEqual(test_step.cluster, 'TestOverride')
self.assertEqual(test_step.endpoint, 4)
def test_config_override_unknown_field(self):
config_override = {'unknown_field': 1}
parser_config = TestParserConfig(
None, self._definitions, config_override)
yaml_parser = TestParser(simple_test_yaml, parser_config)
self.assertIsInstance(yaml_parser, TestParser)
def test_config_valid_enum_values(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(enum_values_yaml, parser_config)
self.assertIsInstance(yaml_parser, TestParser)
for idx, test_step in enumerate(yaml_parser.tests):
pass
def test_config_read_response_wrong_code(self):
parser_config = TestParserConfig(None, self._definitions)
self.assertRaises(TestStepEnumError, TestParser,
enum_value_read_response_wrong_code_yaml, parser_config)
def test_config_read_response_wrong_name(self):
parser_config = TestParserConfig(None, self._definitions)
self.assertRaises(TestStepEnumError, TestParser,
enum_value_read_response_wrong_name_yaml, parser_config)
def test_config_read_response_wrong_code_specified(self):
parser_config = TestParserConfig(None, self._definitions)
self.assertRaises(TestStepEnumSpecifierWrongError, TestParser,
enum_value_read_response_wrong_code_specified_yaml, parser_config)
def test_config_read_response_not_unknown_code_specified(self):
parser_config = TestParserConfig(None, self._definitions)
self.assertRaises(TestStepEnumSpecifierNotUnknownError, TestParser,
enum_value_read_response_not_unknown_code_specified_yaml, parser_config)
def test_revision_no_injection_when_not_needed(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(no_revision_check_yaml, parser_config)
steps = list(yaml_parser.tests)
# Original YAML has 3 tests. It should remain untouched.
self.assertEqual(len(steps), 3)
self.assertEqual(steps[0].label, "Step 1 - No revision check")
self.assertEqual(steps[1].label, "Step 2 - No revision check")
self.assertEqual(steps[2].label, "Step 3 - No revision check")
def test_revision_step_injection(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(
min_max_revision_injection_yaml, parser_config)
# The parser's YAML had 3 steps. The injection logic should add one
# `readAttribute` step before step 2 (which is index 1).
# The parser filters disabled steps, but here we have none.
# The injection happens in YamlTests.__init__
steps = list(yaml_parser.tests)
# Original YAML has 3 tests. We should have 1 read operation injection.
self.assertEqual(len(steps), 4)
# Step index 0 should be the original "Step 1 - No revision check"
self.assertEqual(steps[0].label, "Step 1 - No revision check")
# Step index 1 should be the *injected* step
self.assertEqual(
steps[1].label, "Read ClusterRevision for conditions (EP: 1, CL: Test)")
self.assertEqual(steps[1].command, "readAttribute")
self.assertEqual(steps[1].attribute, "ClusterRevision")
self.assertEqual(
steps[1].responses[0]['values'][0]['saveAs'], build_revision_var_name(endpoint=1, cluster="Test"))
# Step index 2 should be the original "Step 2 - With revision check"
self.assertEqual(steps[2].label, "Step 2 - With revision check")
self.assertEqual(steps[2].min_revision, 5)
# Step index 3 should be the original "Step 3 - Also with revision check"
self.assertEqual(steps[3].label, "Step 3 - Also with revision check")
self.assertEqual(steps[3].max_revision, 10)
def test_revision_step_multi_injection(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(
min_max_revision_multi_injection_yaml, parser_config)
steps = list(yaml_parser.tests)
# Original YAML has 4 tests. We should have 2 injections (one for EP1, one for EP2).
self.assertEqual(len(steps), 6)
# Step index 0: Injected for (EP: 1, CL: Test)
self.assertEqual(
steps[0].label, "Read ClusterRevision for conditions (EP: 1, CL: Test)")
self.assertEqual(steps[0].endpoint, 1)
self.assertEqual(
steps[0].responses[0]['values'][0]['saveAs'], build_revision_var_name(endpoint=1, cluster="Test"))
# Step index 1: Original "Step 1 - EP 1"
self.assertEqual(steps[1].label, "Step 1 - EP 1")
self.assertEqual(steps[1].endpoint, 1)
# Step index 2: Injected for (EP: 2, CL: Test)
self.assertEqual(
steps[2].label, "Read ClusterRevision for conditions (EP: 2, CL: Test)")
self.assertEqual(steps[2].endpoint, 2)
self.assertEqual(
steps[2].responses[0]['values'][0]['saveAs'], build_revision_var_name(endpoint=2, cluster="Test"))
# Step index 3: Original "Step 2 - EP 2"
self.assertEqual(steps[3].label, "Step 2 - EP 2")
self.assertEqual(steps[3].endpoint, 2)
# Step index 4: Original "Step 3 - EP 1 again" (no new injection)
self.assertEqual(steps[4].label, "Step 3 - EP 1 again")
self.assertEqual(steps[4].endpoint, 1)
# Step index 5: Original "Step 4 - EP 2 again" (no new injection)
self.assertEqual(steps[5].label, "Step 4 - EP 2 again")
self.assertEqual(steps[5].endpoint, 2)
def _get_revision_check_between_5_and_10_test_step(self, revision_value):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(min_max_revision_check_yaml, parser_config)
# Manually set the runtime variable to simulate the read step.
# The injected step (list(yaml_parser.tests)[0]) set this variable
# during a read that the runner would do for real.
# We are checking the step that *uses* it (list(yaml_parser.tests)[1]).
cluster_rev_var_name = build_revision_var_name(
endpoint=1, cluster="Test")
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=revision_value)
# Get the step that has the min/max check.
# This is the 2nd step (index 1) after the injected step (index 0).
test_step = list(yaml_parser.tests)[1]
# The step from the YAML has min: 5, max: 10
self.assertEqual(test_step.min_revision, 5)
self.assertEqual(test_step.max_revision, 10)
return test_step
def test_revision_check_in_range(self):
# 7 is between 5 and 10
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=7)
self.assertTrue(test_step.is_revision_condition_passed)
def test_revision_check_in_range_min_boundary(self):
# 5 is between 5 and 10 (inclusive)
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=5)
self.assertTrue(test_step.is_revision_condition_passed)
def test_revision_check_in_range_max_boundary(self):
# 10 is between 5 and 10 (inclusive)
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=10)
self.assertTrue(test_step.is_revision_condition_passed)
def test_revision_check_out_of_range_min(self):
# 4 is less than 5
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=4)
self.assertFalse(test_step.is_revision_condition_passed)
def test_revision_check_out_of_range_max(self):
# 11 is greater than 10
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=11)
self.assertFalse(test_step.is_revision_condition_passed)
def test_revision_check_variable_not_set(self):
# Variable is not set (simulated by passing None): this is illegal and raises
test_step = self._get_revision_check_between_5_and_10_test_step(
revision_value=None)
with self.assertRaises(KeyError):
test_step.is_revision_condition_passed
def test_revision_check_only_min(self):
# This test needs a slightly different YAML
yaml_content = '''
name: Test Min/Max Revision Check
config: { cluster: "Test", endpoint: 1 }
tests:
- label: "Step 1"
command: "test"
minRevision: 5
'''
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(yaml_content, parser_config)
cluster_rev_var_name = build_revision_var_name(
endpoint=1, cluster="Test")
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=4)
test_step_fail = list(yaml_parser.tests)[1]
self.assertFalse(test_step_fail.is_revision_condition_passed)
yaml_parser = TestParser(yaml_content, parser_config)
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=5)
test_step_pass = list(yaml_parser.tests)[1]
self.assertTrue(test_step_pass.is_revision_condition_passed)
yaml_parser = TestParser(yaml_content, parser_config)
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=100)
test_step_pass_high = list(yaml_parser.tests)[1]
self.assertTrue(test_step_pass_high.is_revision_condition_passed)
def test_revision_check_only_max(self):
# This test needs a slightly different YAML
yaml_content = '''
name: Test Min/Max Revision Check
config: { cluster: "Test", endpoint: 1 }
tests:
- label: "Step 1"
command: "test"
maxRevision: 10
'''
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(yaml_content, parser_config)
cluster_rev_var_name = build_revision_var_name(
endpoint=1, cluster="Test")
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=11)
test_step_fail = list(yaml_parser.tests)[1]
self.assertFalse(test_step_fail.is_revision_condition_passed)
yaml_parser = TestParser(yaml_content, parser_config)
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=10)
test_step_pass = list(yaml_parser.tests)[1]
self.assertTrue(test_step_pass.is_revision_condition_passed)
yaml_parser = TestParser(yaml_content, parser_config)
yaml_parser.tests.set_runtime_variable(
name=cluster_rev_var_name, value=2)
test_step_pass_low = list(yaml_parser.tests)[1]
self.assertTrue(test_step_pass_low.is_revision_condition_passed)
def test_revision_check_no_revision_at_all(self):
parser_config = TestParserConfig(None, self._definitions)
yaml_parser = TestParser(no_revision_check_yaml, parser_config)
test_step_fail = list(yaml_parser.tests)[0]
self.assertTrue(test_step_fail.is_revision_condition_passed)
def main():
unittest.main()
if __name__ == '__main__':
main()