Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helper script to diff spec revisions #36916

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions scripts/spec_xml/spec_revision_diff_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#
# Copyright (c) 2024 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.
#
# This script gives a print out of the differences between the two specified spec
# versions and a description of the provisional elements in the later version.
# Right now, this is just in print form. The intent is to use this for new
# data model XML drops to show the differences. This was also used to double-check
# spec expectations before the 1.4 release and we should continue to do so going forward.

import click

from chip.testing.spec_parsing import build_xml_clusters, build_xml_device_types, PrebuiltDataModelDirectory
from chip.testing.conformance import ConformanceDecision


def get_changes(old, new):
added = [e.name for id, e in new.items() if id not in old.keys()]
removed = [e.name for id, e in old.items() if id not in new.keys()]
same_ids = set(new.keys()).intersection(set(old.keys()))

return added, removed, same_ids


def str_changes(element, added, removed, change_ids, old, new):
if not added and not removed and not change_ids:
return []

ret = []
if added:
ret.append(f'\t{element} added: {added}')
if removed:
ret.append(f'\t{element} removed: {removed}')
if change_ids:
ret.append(f'\t{element} changed:')
for id in change_ids:
name = old[id].name if old[id].name == new[id].name else f'{new[id].name} (previously {old[id].name})'
ret.append(f'\t\t{name}')
ret.append(f'\t\t\t{old[id]}')
ret.append(f'\t\t\t{new[id]}')
return ret


def str_element_changes(element, old, new):
added, removed, same_ids = get_changes(old, new)
change_ids = [id for id in same_ids if old[id] != new[id] or str(old[id].conformance) != str(new[id].conformance)]
return str_changes(element, added, removed, change_ids, old, new)


def diff_clusters(prior_revision: PrebuiltDataModelDirectory, new_revision: PrebuiltDataModelDirectory) -> None:
prior_clusters, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_3)
new_clusters, _ = build_xml_clusters(PrebuiltDataModelDirectory.k1_4)

additional_clusters, removed_clusters, same_cluster_ids = get_changes(prior_clusters, new_clusters)

print(f'\n\nClusters newly added in {new_revision.dirname}')
print(additional_clusters)
print(f'\n\nClusters removed since {prior_revision.dirname}')
print(removed_clusters)

for cid in same_cluster_ids:
new = new_clusters[cid]
old = prior_clusters[cid]

name = old.name if old.name == new.name else f'{new.name} (previously {old.name})'

changes = []
if old.revision != new.revision:
changes.append(f'\tRevision change - old: {old.revision} new: {new.revision}')
changes.extend(str_element_changes('Features', old.features, new.features))
changes.extend(str_element_changes('Attributes', old.attributes, new.attributes))
changes.extend(str_element_changes('Accepted Commands', old.accepted_commands, new.accepted_commands))
changes.extend(str_element_changes('Generated Commands', old.generated_commands, new.generated_commands))
changes.extend(str_element_changes('Events', old.events, new.events))

if changes:
print(f'\n\nCluster {name}')
print('\n'.join(changes))


def diff_device_types(prior_revision: PrebuiltDataModelDirectory, new_revision: PrebuiltDataModelDirectory) -> None:
prior_device_types, _ = build_xml_device_types(prior_revision)
new_device_types, _ = build_xml_device_types(new_revision)

additional_device_types, removed_device_types, same_device_type_ids = get_changes(prior_device_types, new_device_types)

print(f'\n\nDevice Types newly added in {new_revision.dirname}')
print(additional_device_types)
print(f'\n\nDevice Types removed since {prior_revision.dirname}')
print(removed_device_types)

for cid in same_device_type_ids:
new = new_device_types[cid]
old = prior_device_types[cid]

name = old.name if old.name == new.name else f'{new.name} (previously {old.name})'

changes = []
if old.revision != new.revision:
changes.append(f'\tRevision change - old: {old.revision} new: {new.revision}')
changes.extend(str_element_changes('Server Clusters', old.server_clusters, new.server_clusters))
changes.extend(str_element_changes('Client Clusters', old.client_clusters, new.client_clusters))

if changes:
print(f'\n\nDevice Type {name}')
print('\n'.join(changes))


def _get_provisional(items):
return [e.name for e in items if e.conformance(0, [], []).decision == ConformanceDecision.PROVISIONAL]


def get_all_provisional_clusters(new_revision: PrebuiltDataModelDirectory):
clusters, _ = build_xml_clusters(new_revision)

provisional_clusters = [c.name for c in clusters.values() if c.is_provisional]
print('\n\nProvisional Clusters')
print(f'\t{sorted(provisional_clusters)}')

for c in clusters.values():
features = _get_provisional(c.features.values())
attributes = _get_provisional(c.attributes.values())
accepted_commands = _get_provisional(c.accepted_commands.values())
generated_commands = _get_provisional(c.generated_commands.values())
events = _get_provisional(c.events.values())

if not features and not attributes and not accepted_commands and not generated_commands and not events:
continue

print(f'\n{c.name}')
if features:
print(f'\tProvisional features: {features}')
if attributes:
print(f'\tProvisional attributes: {attributes}')
if accepted_commands:
print(f'\tProvisional accepted commands: {accepted_commands}')
if generated_commands:
print(f'\tProvisional generated commands: {generated_commands}')
if events:
print(f'\tProvisional events: {events}')


def get_all_provisional_device_types(new_revision: PrebuiltDataModelDirectory):
device_types, _ = build_xml_device_types(new_revision)

for d in device_types.values():
server_clusters = _get_provisional(d.server_clusters.values())
client_clusters = _get_provisional(d.client_clusters.values())
if not server_clusters and not client_clusters:
continue

print(f'\n{d.name}')
if server_clusters:
print(f'\tProvisional server clusters: {server_clusters}')
if client_clusters:
print(f'\tProvisional client clusters: {client_clusters}')


REVISIONS = {'1.3': PrebuiltDataModelDirectory.k1_3,
'1.4': PrebuiltDataModelDirectory.k1_4, 'master': PrebuiltDataModelDirectory.kMaster}


@click.command()
@click.argument('prior_revision', type=click.Choice(list(REVISIONS.keys())))
@click.argument('new_revision', type=click.Choice(list(REVISIONS.keys())))
def main(prior_revision: str, new_revision: str):
diff_clusters(REVISIONS[prior_revision], REVISIONS[new_revision])
diff_device_types(REVISIONS[prior_revision], REVISIONS[new_revision])
get_all_provisional_clusters(REVISIONS[new_revision])
get_all_provisional_device_types(REVISIONS[new_revision])


if __name__ == "__main__":
main()
Loading