blob: e94e79abd5fce5b6c1dcf1d8d3bebf4065370b50 [file] [log] [blame]
#!/usr/bin/env python3
# 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 logging
import unittest
from enum import Flag, auto
try:
from matter_idl.matter_idl_parser import CreateParser
except ImportError:
import os
import sys
sys.path.append(os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
from matter_idl.matter_idl_parser import CreateParser
from matter_idl.backwards_compatibility import CompatibilityChecker, is_backwards_compatible
from matter_idl.matter_idl_types import Idl
class Compatibility(Flag):
ALL_OK = auto() # placeholder allowing anything
FORWARD_FAIL = auto() # old -> new is wrong
BACKWARD_FAIL = auto() # new -> old is wrong
class DisableLogger():
def __enter__(self):
logging.disable(logging.CRITICAL)
def __exit__(self, exit_type, exit_value, exit_traceback):
logging.disable(logging.NOTSET)
class TestCompatibilityChecks(unittest.TestCase):
def _AssumeCompatiblity(self, old: str, new: str, old_idl: Idl, new_idl: Idl, expect_compatible: bool):
with DisableLogger():
if expect_compatible == is_backwards_compatible(old_idl, new_idl):
return
# re-run to figure out reasons:
checker = CompatibilityChecker(old_idl, new_idl)
checker.check()
if expect_compatible:
reason = "EXPECTED COMPATIBLE, but failed"
fail_reasons = "\nREASONS:" + "\n - ".join([""] + checker.errors)
else:
reason = "EXPECTED NOT COMPATIBLE, but succeeded"
fail_reasons = ""
self.fail(f"""Failed compatibility test
{reason}:
/-------------------- OLD ----------------/
{old}
/-------------------- NEW ----------------/
{new}
/-----------------------------------------/{fail_reasons}""")
def ValidateUpdate(self, name: str, old: str, new: str, flags: Compatibility):
old_idl = CreateParser(skip_meta=True).parse(old)
new_idl = CreateParser(skip_meta=True).parse(new)
with self.subTest(validate=name):
# Validate compatibility and that no changes are always compatible
with self.subTest(direction="Forward"):
self._AssumeCompatiblity(
old, new, old_idl, new_idl, Compatibility.FORWARD_FAIL not in flags)
with self.subTest(direction="Backward"):
self._AssumeCompatiblity(
new, old, new_idl, old_idl, Compatibility.BACKWARD_FAIL not in flags)
with self.subTest(direction="NEW-to-NEW"):
self._AssumeCompatiblity(new, new, new_idl, new_idl, True)
with self.subTest(direction="OLD-to-OLD"):
self._AssumeCompatiblity(old, old, old_idl, old_idl, True)
def test_basic_clusters_enum(self):
self.ValidateUpdate(
"Adding an enum is ok. Also validates code formatting",
"server cluster A = 16 {}",
"server cluster A = 0x10 { enum X : ENUM8 {} }",
Compatibility.BACKWARD_FAIL)
def test_clusters_enum_add_remove(self):
self.ValidateUpdate(
"Adding an enum is ok. Also validates code formatting",
"server cluster A = 16 { enum X : ENUM8 { A = 1; B = 2; }}",
"server cluster A = 16 { enum X : ENUM8 { A = 1; }}",
Compatibility.FORWARD_FAIL)
def test_provisional_cluster(self):
self.ValidateUpdate(
"Provisional cluster changes are ok.",
"provisional server cluster A = 16 { enum X : ENUM8 { A = 1; B = 2; } info event A = 1 { int8u x = 1;} }",
"provisional server cluster A = 16 { enum X : ENUM8 { A = 1; B = 3; } info event A = 2 { int16u x = 1;} }",
Compatibility.ALL_OK)
def test_clusters_enum_code(self):
self.ValidateUpdate(
"Adding an enum is ok. Also validates code formatting",
"server cluster A = 16 { enum X : ENUM8 { A = 1; B = 2; }}",
"server cluster A = 16 { enum X : ENUM8 { A = 1; B = 3; }}",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_clusters_enum_delete(self):
self.ValidateUpdate(
"Adding an enum is ok. Also validates code formatting",
"server cluster A = 16 { enum X : ENUM8 {}}",
"server cluster A = 16 { }",
Compatibility.FORWARD_FAIL)
def test_clusters_enum_change(self):
self.ValidateUpdate(
"Adding an enum is ok. Also validates code formatting",
"server cluster A = 16 { enum X : ENUM16 {}}",
"server cluster A = 16 { enum X : ENUM8 {}}",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_bitmaps_delete(self):
self.ValidateUpdate(
"Deleting a bitmap is not ok",
"client Cluster X = 1 { bitmap A: BITMAP8{} bitmap B: BITMAP8{} }",
"client Cluster X = 1 { bitmap A: BITMAP8{} }",
Compatibility.FORWARD_FAIL)
def test_bitmaps_type(self):
self.ValidateUpdate(
"Changing a bitmap type is never ok",
" client cluster X = 1 { bitmap A: BITMAP8{} }",
" client cluster X = 1 { bitmap A: BITMAP16{} }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_bitmaps_values(self):
self.ValidateUpdate(
"Adding bitmap values is ok, removing values is not",
" client cluster X = 1 { bitmap A: BITMAP8 { kA = 0x01; kB = 0x02; } }",
" client cluster X = 1 { bitmap A: BITMAP8 { kA = 0x01; } }",
Compatibility.FORWARD_FAIL)
def test_bitmaps_code(self):
self.ValidateUpdate(
"Switching bitmap codes is never ok",
" client cluster X = 1 { bitmap A: BITMAP8 { kA = 0x01; kB = 0x02; } }",
" client cluster X = 1 { bitmap A: BITMAP8 { kA = 0x01; kB = 0x04; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_events_delete(self):
self.ValidateUpdate(
"Deleting an event is not ok",
"client Cluster X = 1 { info event A = 1 {} info event B = 2 {} }",
"client Cluster X = 1 { info event B = 2 {} }",
Compatibility.FORWARD_FAIL)
def test_events_code(self):
self.ValidateUpdate(
"Changing event code is never ok",
"client Cluster X = 1 { info event A = 1 {} }",
"client Cluster X = 1 { info event A = 2 {} }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_events_struct_content_change(self):
self.ValidateUpdate(
"Changing event struct is never ok",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; } }",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; int8u y = 2; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_events_struct_renames(self):
self.ValidateUpdate(
"Renaming struct content is ok",
"client Cluster X = 1 { info event A = 1 { int8u a = 1; int8u b = 2; } }",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; int8u y = 2; } }",
Compatibility.ALL_OK)
def test_events_struct_reorders_renames(self):
self.ValidateUpdate(
"Renaming struct content is ok",
"client Cluster X = 1 { info event A = 1 { int8u a = 1; int16u b = 2; } }",
"client Cluster X = 1 { info event A = 1 { int16u x = 2; int8u a = 1; } }",
Compatibility.ALL_OK)
def test_events_struct_type_change(self):
self.ValidateUpdate(
"Changing event struct is never ok",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; } }",
"client Cluster X = 1 { info event A = 1 { int16u x = 1; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_events_struct_id_change(self):
self.ValidateUpdate(
"Changing event struct is never ok",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; } }",
"client Cluster X = 1 { info event A = 1 { int8u x = 2; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_events_severity(self):
self.ValidateUpdate(
"Changing event severity is ok",
"client Cluster X = 1 { info event A = 1 { int8u x = 1; } }",
"client Cluster X = 1 { critical event A = 1 { int8u x = 1; } }",
Compatibility.ALL_OK)
def test_commands_remove(self):
self.ValidateUpdate(
"Removing commands is not ok",
"client Cluster X = 1 { command A() : DefaulSuccess = 0; command B() : DefaultSuccess = 1; }",
"client Cluster X = 1 { command A() : DefaulSuccess = 0; }",
Compatibility.FORWARD_FAIL)
def test_commands_id(self):
self.ValidateUpdate(
"Changing command IDs is never ok",
"client Cluster X = 1 { command A() : DefaulSuccess = 1; }",
"client Cluster X = 1 { command A() : DefaulSuccess = 2; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_commands_input(self):
self.ValidateUpdate(
"Changing command Inputs is never ok",
"client Cluster X = 1 { command A() : DefaulSuccess = 1; }",
"client Cluster X = 1 { command A(ARequest) : DefaulSuccess = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_commands_output(self):
self.ValidateUpdate(
"Changing command Outputs is never ok",
"client Cluster X = 1 { command A() : DefaultSuccess = 1; }",
"client Cluster X = 1 { timed command A() : DefaultSuccess = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_commands_quality_fabric(self):
self.ValidateUpdate(
"Changing command quality is not ok",
"client Cluster X = 1 { command A() : DefaultSuccess = 1; }",
"client Cluster X = 1 { fabric command A() : DefaultSuccess = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_commands_quality_timed(self):
self.ValidateUpdate(
"Changing command qualities is never ok",
"client Cluster X = 1 { fabric command A() : DefaultSuccess = 1; }",
"client Cluster X = 1 { fabric timed command A() : DefaultSuccess = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_commands_change_acl(self):
self.ValidateUpdate(
"Switching access is ok",
"client Cluster X = 1 { command A() : DefaultSuccess = 1; }",
"client Cluster X = 1 { command access(invoke: administer) A() : DefaultSuccess = 1; }",
Compatibility.ALL_OK)
def test_struct_removal(self):
self.ValidateUpdate(
"Structure removal is not ok, but adding is ok",
"client Cluster X = 1 { struct Foo {} struct Bar {} }",
"client Cluster X = 1 { struct Foo {} }",
Compatibility.FORWARD_FAIL)
def test_struct_content_type_change(self):
self.ValidateUpdate(
"Changing structure data types is never ok",
"client Cluster X = 1 { struct Foo { int32u x = 1; } }",
"client Cluster X = 1 { struct Foo { int64u x = 1; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_struct_content_rename_reorder(self):
self.ValidateUpdate(
"Structure content renames and reorder is ok.",
"client Cluster X = 1 { struct Foo { int32u x = 1; int8u y = 2; } }",
"client Cluster X = 1 { struct Foo { int8u a = 2; int32u y = 1; } }",
Compatibility.ALL_OK)
def test_struct_content_add_remove(self):
self.ValidateUpdate(
"Structure content change is not ok.",
"client Cluster X = 1 { struct Foo { int32u x = 1; } }",
"client Cluster X = 1 { struct Foo { int32u x = 1; int8u y = 2; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_struct_tag_change_request(self):
self.ValidateUpdate(
"Structure type (request/regular) change is not ok",
"client Cluster X = 1 { struct Foo { int32u x = 1; } }",
"client Cluster X = 1 { request struct Foo { int32u x = 1; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_struct_tag_change_response(self):
self.ValidateUpdate(
"Structure type (request/response) change is not ok",
"client Cluster X = 1 { request struct Foo { int32u x = 1; } }",
"client Cluster X = 1 { response struct Foo = 1 { int32u x = 1; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_struct_code(self):
self.ValidateUpdate(
"Changing struct code is not ok",
"client Cluster X = 1 { response struct Foo = 1 { int32u x = 1; } }",
"client Cluster X = 1 { response struct Foo = 2 { int32u x = 1; } }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_add_remove(self):
self.ValidateUpdate(
"Attribute removal is not ok.",
"client Cluster X = 1 { readonly attribute int8u a = 1; readonly attribute int8u b = 2; }",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
Compatibility.FORWARD_FAIL)
def test_attribute_change_type(self):
self.ValidateUpdate(
"Attribute type cannot be changed.",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
"client Cluster X = 1 { readonly attribute int32u a = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_change_code(self):
self.ValidateUpdate(
"Attribute codes cannot be changed.",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
"client Cluster X = 1 { readonly attribute int8u a = 2; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_change_optionality(self):
self.ValidateUpdate(
"Attribute optionality must stay the same",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
"client Cluster X = 1 { readonly attribute optional int8u a = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_change_list(self):
self.ValidateUpdate(
"Attribute list type must stay the same",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
"client Cluster X = 1 { readonly attribute int8u a[] = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_change_rw(self):
self.ValidateUpdate(
"Attribute read/write status must stay the same",
"client Cluster X = 1 { readonly attribute int8u a = 1; }",
"client Cluster X = 1 { attribute int8u a = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
def test_attribute_type(self):
self.ValidateUpdate(
"Attribute data type must stay the same",
"client Cluster X = 1 { attribute int8u a = 1; }",
"client Cluster X = 1 { attribute char_string a = 1; }",
Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL)
if __name__ == '__main__':
unittest.main()