From 150681885107d6308aea9c15dd5b00791b030df1 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 21 Sep 2023 12:15:39 -0400 Subject: [PATCH] Add a script capable to check for backwards compatibility differences between .matter files (#29349) * Start defining a compatibility checker with unit tests * Enumeration validation passes * Restyle * Mark unimplemented items with TODO * Add some cluster validation tests * Add validation for bitmap statuses and unit tests for that * Mild restyle and clearer code * Take into consideration cluster sides * Update naming a bit * More detailed messages for code entry changes * Use sub-tests to mark clearly what failures occur where * Fix typing and common logic * Add implementation and tests for event backward compatibility * Restyle * Add command backwards compatibility checks and tests, better error reporting * Less noisy output for unit tests * Restyle * Fixed types * Split tests so we have better split and execution report - 23 tests instead of just 5 * Add minor unit test * Added struct handling and unit tests * Restyle * Attribute validation * Restyle * Fix typo * Add some extra logic to not detect renames in fields * Additional tests for renames of fields * Add struct tests for reorder and renames * Add cluster names for error reports * Restyle * Fix test comments * Move defs outside init into separate file, update build scripts * Move to single file for use as an import instead of a module directory * Remove unused import * Rename some global methods to pep8 * Move more methods to pep8 * Remove unused import * Make sure unit test is run as part of standard tests * Update comment * Ensure click argument for choice is list of str instead of a dictionary view (makes mypy happy) --------- Co-authored-by: Andrei Litvin --- scripts/backwards_compatibility_checker.py | 90 +++++ scripts/py_matter_idl/BUILD.gn | 1 + scripts/py_matter_idl/files.gni | 2 + .../matter_idl/backwards_compatibility.py | 291 ++++++++++++++ .../test_backwards_compatibility.py | 380 ++++++++++++++++++ .../java/AndroidDeviceControllerWrapper.cpp | 9 +- 6 files changed, 768 insertions(+), 5 deletions(-) create mode 100755 scripts/backwards_compatibility_checker.py create mode 100644 scripts/py_matter_idl/matter_idl/backwards_compatibility.py create mode 100755 scripts/py_matter_idl/matter_idl/test_backwards_compatibility.py diff --git a/scripts/backwards_compatibility_checker.py b/scripts/backwards_compatibility_checker.py new file mode 100755 index 00000000000000..ac5f2f094919fb --- /dev/null +++ b/scripts/backwards_compatibility_checker.py @@ -0,0 +1,90 @@ +#!/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, +# See the License for the specific language governing permissions and +# limitations under the License. +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +import logging +import sys + +import click + +try: + import coloredlogs + _has_coloredlogs = True +except ImportError: + _has_coloredlogs = False + +try: + from matter_idl.matter_idl_parser import CreateParser +except ImportError: + import os + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'py_matter_idl'))) + from matter_idl.matter_idl_parser import CreateParser + +from matter_idl.backwards_compatibility import is_backwards_compatible + +# Supported log levels, mapping string values required for argument +# parsing into logging constants +__LOG_LEVELS__ = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'fatal': logging.FATAL, +} + + +@click.command() +@click.option( + '--log-level', + default='INFO', + type=click.Choice(list(__LOG_LEVELS__.keys()), case_sensitive=False), + help='Determines the verbosity of script output') +@click.argument( + 'old_idl', + type=click.Path(exists=True)) +@click.argument( + 'new_idl', + type=click.Path(exists=True)) +def main(log_level, old_idl, new_idl): + """ + Parses MATTER IDL files (.matter) and validates that is backwards compatible + when compared to . + + Generally additions are safe, but not deletes or id changes. Actual set of rules + defined in `backwards_compatibility` module. + """ + if _has_coloredlogs: + coloredlogs.install(level=__LOG_LEVELS__[ + log_level], fmt='%(asctime)s %(levelname)-7s %(message)s') + else: + logging.basicConfig( + level=__LOG_LEVELS__[log_level], + format='%(asctime)s %(levelname)-7s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + logging.info("Parsing OLD idl from %s" % old_idl) + old_tree = CreateParser().parse(open(old_idl, "rt").read()) + + logging.info("Parsing NEW idl from %s" % new_idl) + new_tree = CreateParser().parse(open(new_idl, "rt").read()) + + if not is_backwards_compatible(original=old_tree, updated=new_tree): + sys.exit(1) + + sys.exit(0) + + +if __name__ == '__main__': + main(auto_envvar_prefix='CHIP') diff --git a/scripts/py_matter_idl/BUILD.gn b/scripts/py_matter_idl/BUILD.gn index bd1975afe3b4c0..81f5be3be554b4 100644 --- a/scripts/py_matter_idl/BUILD.gn +++ b/scripts/py_matter_idl/BUILD.gn @@ -59,6 +59,7 @@ pw_python_package("matter_idl") { sources = matter_idl_generator_sources tests = [ + "matter_idl/test_backwards_compatibility.py", "matter_idl/test_matter_idl_parser.py", "matter_idl/test_generators.py", "matter_idl/test_xml_parser.py", diff --git a/scripts/py_matter_idl/files.gni b/scripts/py_matter_idl/files.gni index bfcfd27aec6fcd..84680a8863d42f 100644 --- a/scripts/py_matter_idl/files.gni +++ b/scripts/py_matter_idl/files.gni @@ -21,6 +21,7 @@ matter_idl_generator_templates = [ matter_idl_generator_sources = [ "${chip_root}/scripts/py_matter_idl/matter_idl/__init__.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/backwards_compatibility.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/__init__.py", @@ -34,6 +35,7 @@ matter_idl_generator_sources = [ "${chip_root}/scripts/py_matter_idl/matter_idl/lint/types.py", "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_parser.py", "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_types.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/test_backwards_compatibility.py", "${chip_root}/scripts/py_matter_idl/matter_idl/test_generators.py", "${chip_root}/scripts/py_matter_idl/matter_idl/test_matter_idl_parser.py", "${chip_root}/scripts/py_matter_idl/matter_idl/test_xml_parser.py", diff --git a/scripts/py_matter_idl/matter_idl/backwards_compatibility.py b/scripts/py_matter_idl/matter_idl/backwards_compatibility.py new file mode 100644 index 00000000000000..8ecf45afc26eb8 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/backwards_compatibility.py @@ -0,0 +1,291 @@ +# 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. + +import dataclasses +import enum +import logging +from typing import Callable, Dict, List, Optional, Protocol, TypeVar + +from matter_idl.matter_idl_types import Attribute, Bitmap, Cluster, ClusterSide, Command, Enum, Event, Field, Idl, Struct + + +class Compatibility(enum.Enum): + UNKNOWN = enum.auto() + COMPATIBLE = enum.auto() + INCOMPATIBLE = enum.auto() + + +T = TypeVar('T') + + +class HasName(Protocol): + name: str + + +NAMED = TypeVar('NAMED', bound=HasName) + + +def group_list(items: List[T], get_id: Callable[[T], str]) -> Dict[str, T]: + result = {} + for item in items: + result[get_id(item)] = item + return result + + +def group_list_by_name(items: List[NAMED]) -> Dict[str, NAMED]: + return group_list(items, lambda x: x.name) + + +def full_cluster_name(cluster: Cluster) -> str: + "Builds a unique cluster name considering the side as well" + if cluster.side == ClusterSide.CLIENT: + return f"{cluster.name}/client" + else: + return f"{cluster.name}/server" + + +def attribute_name(attribute: Attribute) -> str: + """Get the name of an attribute.""" + return attribute.definition.name + + +class CompatibilityChecker: + def __init__(self, original: Idl, updated: Idl): + self._original_idl = original + self._updated_idl = updated + self.compatible = Compatibility.UNKNOWN + self.errors: List[str] = [] + self.logger = logging.getLogger(__name__) + + def _mark_incompatible(self, reason: str): + self.logger.error(reason) + self.errors.append(reason) + self.compatible = Compatibility.INCOMPATIBLE + + def _check_field_lists_are_the_same(self, location: str, original: List[Field], updated: List[Field]): + """Validates no compatibility changes in a list of fields. + + Specifically no changes are allowed EXCEPT names of fields. + """ + + # Every field MUST be the same except that + # name does not matter and order does not matter + # + # Comparison is done on a dict (so order does not matter) + # and replacing names with a fixed name based on code. + a = {} + for item in original: + a[item.code] = dataclasses.replace(item, name=f"entry{item.code}") + + b = {} + for item in updated: + b[item.code] = dataclasses.replace(item, name=f"entry{item.code}") + + if a != b: + self._mark_incompatible(f"{location} has field changes") + + def _check_enum_compatible(self, cluster_name: str, original: Enum, updated: Optional[Enum]): + if not updated: + self._mark_incompatible(f"Enumeration {cluster_name}::{original.name} was deleted") + return + + if original.base_type != updated.base_type: + self._mark_incompatible( + f"Enumeration {cluster_name}::{original.name} switched base type from {original.base_type} to {updated.base_type}") + + # Validate that all old entries exist + for entry in original.entries: + # old entry must exist and have identical code + existing = [item for item in updated.entries if item.name == entry.name] + if len(existing) == 0: + self._mark_incompatible(f"Enumeration {cluster_name}::{original.name} removed entry {entry.name}") + elif existing[0].code != entry.code: + self._mark_incompatible( + f"Enumeration {cluster_name}::{original.name} changed code for entry {entry.name} from {entry.code} to {existing[0].code}") + + def _check_bitmap_compatible(self, cluster_name: str, original: Bitmap, updated: Optional[Bitmap]): + if not updated: + self._mark_incompatible(f"Bitmap {cluster_name}::{original.name} was deleted") + return + + if original.base_type != updated.base_type: + self._mark_incompatible( + f"Bitmap {cluster_name}::{original.name} switched base type from {original.base_type} to {updated.base_type}") + + # Validate that all old entries exist + for entry in original.entries: + # old entry must exist and have identical code + existing = [item for item in updated.entries if item.name == entry.name] + if len(existing) == 0: + self._mark_incompatible(f"Bitmap {original.name} removed entry {entry.name}") + elif existing[0].code != entry.code: + self._mark_incompatible( + f"Bitmap {original.name} changed code for entry {entry.name} from {entry.code} to {existing[0].code}") + + def _check_event_compatible(self, cluster_name: str, event: Event, updated_event: Optional[Event]): + if not updated_event: + self._mark_incompatible(f"Event {cluster_name}::{event.name} was removed") + return + + if event.code != updated_event.code: + self._mark_incompatible(f"Event {cluster_name}::{event.name} code changed from {event.code} to {updated_event.code}") + + self._check_field_lists_are_the_same(f"Event {cluster_name}::{event.name}", event.fields, updated_event.fields) + + def _check_command_compatible(self, cluster_name: str, command: Command, updated_command: Optional[Command]): + self.logger.debug(f" Checking command {cluster_name}::{command.name}") + if not updated_command: + self._mark_incompatible(f"Command {cluster_name}::{command.name} was removed") + return + + if command.code != updated_command.code: + self._mark_incompatible( + f"Command {cluster_name}::{command.name} code changed from {command.code} to {updated_command.code}") + + if command.input_param != updated_command.input_param: + self._mark_incompatible( + f"Command {cluster_name}::{command.name} input changed from {command.input_param} to {updated_command.input_param}") + + if command.output_param != updated_command.output_param: + self._mark_incompatible( + f"Command {cluster_name}::{command.name} output changed from {command.output_param} to {updated_command.output_param}") + + if command.qualities != updated_command.qualities: + self._mark_incompatible( + f"Command {cluster_name}::{command.name} qualities changed from {command.qualities} to {updated_command.qualities}") + + def _check_struct_compatible(self, cluster_name: str, original: Struct, updated: Optional[Struct]): + self.logger.debug(f" Checking struct {original.name}") + if not updated: + self._mark_incompatible(f"Struct {cluster_name}::{original.name} has been deleted.") + return + + self._check_field_lists_are_the_same(f"Struct {cluster_name}::{original.name}", original.fields, updated.fields) + + if original.tag != updated.tag: + self._mark_incompatible(f"Struct {cluster_name}::{original.name} has modified tags") + + if original.code != updated.code: + self._mark_incompatible(f"Struct {cluster_name}::{original.name} has modified code (likely resnopse difference)") + + if original.qualities != updated.qualities: + self._mark_incompatible(f"Struct {cluster_name}::{original.name} has modified qualities") + + def _check_attribute_compatible(self, cluster_name: str, original: Attribute, updated: Optional[Attribute]): + self.logger.debug(f" Checking attribute {cluster_name}::{original.definition.name}") + if not updated: + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} has been deleted.") + return + + if original.definition.code != updated.definition.code: + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} changed its code.") + + if original.definition.data_type != updated.definition.data_type: + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} changed its data type.") + + if original.definition.is_list != updated.definition.is_list: + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} changed its list status.") + + if original.definition.qualities != updated.definition.qualities: + # optional/nullable + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} changed its data type qualities.") + + if original.qualities != updated.qualities: + # read/write/subscribe/timed status + self._mark_incompatible(f"Attribute {cluster_name}::{original.definition.name} changed its qualities.") + + def _check_enum_list_compatible(self, cluster_name: str, original: List[Enum], updated: List[Enum]): + updated_enums = group_list_by_name(updated) + + for original_enum in original: + updated_enum = updated_enums.get(original_enum.name) + self._check_enum_compatible(cluster_name, original_enum, updated_enum) + + def _check_bitmap_list_compatible(self, cluster_name: str, original: List[Bitmap], updated: List[Bitmap]): + updated_bitmaps = {} + for item in updated: + updated_bitmaps[item.name] = item + + for original_bitmap in original: + updated_bitmap = updated_bitmaps.get(original_bitmap.name) + self._check_bitmap_compatible(cluster_name, original_bitmap, updated_bitmap) + + def _check_struct_list_compatible(self, cluster_name: str, original: List[Struct], updated: List[Struct]): + updated_structs = group_list_by_name(updated) + + for struct in original: + self._check_struct_compatible(cluster_name, struct, updated_structs.get(struct.name)) + + def _check_command_list_compatible(self, cluster_name: str, original: List[Command], updated: List[Command]): + updated_commands = group_list_by_name(updated) + + for command in original: + updated_command = updated_commands.get(command.name) + self._check_command_compatible(cluster_name, command, updated_command) + + def _check_event_list_compatible(self, cluster_name: str, original: List[Event], updated: List[Event]): + updated_events = group_list_by_name(updated) + + for event in original: + updated_event = updated_events.get(event.name) + self._check_event_compatible(cluster_name, event, updated_event) + + def _check_attribute_list_compatible(self, cluster_name: str, original: List[Attribute], updated: List[Attribute]): + updated_attributes = group_list(updated, attribute_name) + + for attribute in original: + self._check_attribute_compatible(cluster_name, attribute, updated_attributes.get(attribute_name(attribute))) + + def _check_cluster_list_compatible(self, original: List[Cluster], updated: List[Cluster]): + updated_clusters = group_list(updated, full_cluster_name) + + for original_cluster in original: + updated_cluster = updated_clusters.get(full_cluster_name(original_cluster)) + self._check_cluster_compatible(original_cluster, updated_cluster) + + def _check_cluster_compatible(self, original_cluster: Cluster, updated_cluster: Optional[Cluster]): + self.logger.debug(f"Checking cluster {full_cluster_name(original_cluster)}") + if not updated_cluster: + self._mark_incompatible(f"Cluster {full_cluster_name(original_cluster)} was deleted") + return + + if original_cluster.code != updated_cluster.code: + self._mark_incompatible( + f"Cluster {full_cluster_name(original_cluster)} has different codes {original_cluster.code} != {updated_cluster.code}") + + self._check_enum_list_compatible(original_cluster.name, original_cluster.enums, updated_cluster.enums) + self._check_struct_list_compatible(original_cluster.name, original_cluster.structs, updated_cluster.structs) + self._check_bitmap_list_compatible(original_cluster.name, original_cluster.bitmaps, updated_cluster.bitmaps) + self._check_command_list_compatible(original_cluster.name, original_cluster.commands, updated_cluster.commands) + self._check_event_list_compatible(original_cluster.name, original_cluster.events, updated_cluster.events) + self._check_attribute_list_compatible(original_cluster.name, original_cluster.attributes, updated_cluster.attributes) + + def check(self): + # assume ok, and then validate + self.compatible = Compatibility.COMPATIBLE + + self._check_enum_list_compatible("", self._original_idl.enums, self._updated_idl.enums) + self._check_struct_list_compatible("", self._original_idl.structs, self._updated_idl.structs) + self._check_cluster_list_compatible(self._original_idl.clusters, self._updated_idl.clusters) + + return self.compatible + + +def is_backwards_compatible(original: Idl, updated: Idl): + """ + Validate that 'updated' IDL contains only + incremental changes from 'original' + """ + checker = CompatibilityChecker(original, updated) + return checker.check() == Compatibility.COMPATIBLE diff --git a/scripts/py_matter_idl/matter_idl/test_backwards_compatibility.py b/scripts/py_matter_idl/matter_idl/test_backwards_compatibility.py new file mode 100755 index 00000000000000..f50c4f06b03684 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/test_backwards_compatibility.py @@ -0,0 +1,380 @@ +#!/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_top_level_enums_delete(self): + self.ValidateUpdate( + "delete a top level enum", + "enum A: ENUM8{} enum B: ENUM8{}", + "enum A: ENUM8{}", + Compatibility.FORWARD_FAIL) + + def test_top_level_enums_change(self): + self.ValidateUpdate( + "change an enum type", + "enum A: ENUM8{}", + "enum A: ENUM16{}", + Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL) + + def test_top_level_enums_add_remove(self): + self.ValidateUpdate( + "Adding enum values is ok, removing values is not", + "enum A: ENUM8 {A = 1; B = 2;}", + "enum A: ENUM8 {A = 1; }", + Compatibility.FORWARD_FAIL) + + def test_top_level_enums_code(self): + self.ValidateUpdate( + "Switching enum codes is never ok", + "enum A: ENUM8 {A = 1; B = 2; }", + "enum A: ENUM8 {A = 1; B = 3; }", + Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL) + + def test_basic_clusters_code(self): + self.ValidateUpdate( + "Switching cluster codes is never ok", + "client cluster A = 1 {}", + "client cluster A = 2 {}", + Compatibility.FORWARD_FAIL | Compatibility.BACKWARD_FAIL) + + def test_basic_clusters_remove(self): + self.ValidateUpdate( + "Removing a cluster is not ok", + "client cluster A = 1 {} client cluster B = 2 {}", + "client cluster A = 1 {}", + Compatibility.FORWARD_FAIL) + + 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_basic_clusters_side(self): + self.ValidateUpdate( + "Detects side switch for clusters", + "client cluster A = 1 {}", + "server cluster A = 1 {}", + 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() diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp index 0c6ae04e7d2e37..52c836c7d24fdd 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.cpp +++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp @@ -600,8 +600,8 @@ void AndroidDeviceControllerWrapper::OnScanNetworksSuccess( std::string NetworkingStatusClassName = "java/lang/Integer"; std::string NetworkingStatusCtorSignature = "(I)V"; jint jniNetworkingStatus = static_cast(dataResponse.networkingStatus); - chip::JniReferences::GetInstance().CreateBoxedObject( - NetworkingStatusClassName, NetworkingStatusCtorSignature, jniNetworkingStatus, NetworkingStatus); + chip::JniReferences::GetInstance().CreateBoxedObject(NetworkingStatusClassName, NetworkingStatusCtorSignature, + jniNetworkingStatus, NetworkingStatus); jobject DebugText; if (!dataResponse.debugText.HasValue()) { @@ -633,9 +633,8 @@ void AndroidDeviceControllerWrapper::OnScanNetworksSuccess( std::string newElement_securityClassName = "java/lang/Integer"; std::string newElement_securityCtorSignature = "(I)V"; jint jniNewElementSecurity = static_cast(entry.security.Raw()); - chip::JniReferences::GetInstance().CreateBoxedObject(newElement_securityClassName, - newElement_securityCtorSignature, - jniNewElementSecurity, newElement_security); + chip::JniReferences::GetInstance().CreateBoxedObject( + newElement_securityClassName, newElement_securityCtorSignature, jniNewElementSecurity, newElement_security); jobject newElement_ssid; jbyteArray newElement_ssidByteArray = env->NewByteArray(static_cast(entry.ssid.size())); env->SetByteArrayRegion(newElement_ssidByteArray, 0, static_cast(entry.ssid.size()),