xref: /aosp_15_r20/tools/treble/split/xml_diff.py (revision 105f628577ac4ba0e277a494fbb614ed8c12a994)
1*105f6285SAndroid Build Coastguard Worker# Copyright (C) 2020 The Android Open Source Project
2*105f6285SAndroid Build Coastguard Worker#
3*105f6285SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*105f6285SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*105f6285SAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*105f6285SAndroid Build Coastguard Worker#
7*105f6285SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*105f6285SAndroid Build Coastguard Worker#
9*105f6285SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*105f6285SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*105f6285SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*105f6285SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*105f6285SAndroid Build Coastguard Worker# limitations under the License.
14*105f6285SAndroid Build Coastguard Worker"""A library containing functions for diffing XML elements."""
15*105f6285SAndroid Build Coastguard Workerimport textwrap
16*105f6285SAndroid Build Coastguard Workerfrom typing import Any, Callable, Dict, Set
17*105f6285SAndroid Build Coastguard Workerimport xml.etree.ElementTree as ET
18*105f6285SAndroid Build Coastguard Workerimport dataclasses
19*105f6285SAndroid Build Coastguard Worker
20*105f6285SAndroid Build Coastguard WorkerElement = ET.Element
21*105f6285SAndroid Build Coastguard Worker
22*105f6285SAndroid Build Coastguard Worker_INDENT = (' ' * 2)
23*105f6285SAndroid Build Coastguard Worker
24*105f6285SAndroid Build Coastguard Worker
25*105f6285SAndroid Build Coastguard Worker@dataclasses.dataclass
26*105f6285SAndroid Build Coastguard Workerclass Change:
27*105f6285SAndroid Build Coastguard Worker  value_from: str
28*105f6285SAndroid Build Coastguard Worker  value_to: str
29*105f6285SAndroid Build Coastguard Worker
30*105f6285SAndroid Build Coastguard Worker  def __repr__(self):
31*105f6285SAndroid Build Coastguard Worker    return f'{self.value_from} -> {self.value_to}'
32*105f6285SAndroid Build Coastguard Worker
33*105f6285SAndroid Build Coastguard Worker
34*105f6285SAndroid Build Coastguard Worker@dataclasses.dataclass
35*105f6285SAndroid Build Coastguard Workerclass ChangeMap:
36*105f6285SAndroid Build Coastguard Worker  """A collection of changes broken down by added, removed and modified.
37*105f6285SAndroid Build Coastguard Worker
38*105f6285SAndroid Build Coastguard Worker  Attributes:
39*105f6285SAndroid Build Coastguard Worker    added: A dictionary of string identifiers to the added string.
40*105f6285SAndroid Build Coastguard Worker    removed: A dictionary of string identifiers to the removed string.
41*105f6285SAndroid Build Coastguard Worker    modified: A dictionary of string identifiers to the changed object.
42*105f6285SAndroid Build Coastguard Worker  """
43*105f6285SAndroid Build Coastguard Worker  added: Dict[str, str] = dataclasses.field(default_factory=dict)
44*105f6285SAndroid Build Coastguard Worker  removed: Dict[str, str] = dataclasses.field(default_factory=dict)
45*105f6285SAndroid Build Coastguard Worker  modified: Dict[str, Any] = dataclasses.field(default_factory=dict)
46*105f6285SAndroid Build Coastguard Worker
47*105f6285SAndroid Build Coastguard Worker  def __repr__(self):
48*105f6285SAndroid Build Coastguard Worker    ret_str = ''
49*105f6285SAndroid Build Coastguard Worker    if self.added:
50*105f6285SAndroid Build Coastguard Worker      ret_str += 'Added:\n'
51*105f6285SAndroid Build Coastguard Worker      for value in self.added.values():
52*105f6285SAndroid Build Coastguard Worker        ret_str += textwrap.indent(str(value) + '\n', _INDENT)
53*105f6285SAndroid Build Coastguard Worker    if self.removed:
54*105f6285SAndroid Build Coastguard Worker      ret_str += 'Removed:\n'
55*105f6285SAndroid Build Coastguard Worker      for value in self.removed.values():
56*105f6285SAndroid Build Coastguard Worker        ret_str += textwrap.indent(str(value) + '\n', _INDENT)
57*105f6285SAndroid Build Coastguard Worker    if self.modified:
58*105f6285SAndroid Build Coastguard Worker      ret_str += 'Modified:\n'
59*105f6285SAndroid Build Coastguard Worker      for name, value in self.modified.items():
60*105f6285SAndroid Build Coastguard Worker        ret_str += textwrap.indent(name + ':\n', _INDENT)
61*105f6285SAndroid Build Coastguard Worker        ret_str += textwrap.indent(str(value) + '\n', _INDENT * 2)
62*105f6285SAndroid Build Coastguard Worker    return ret_str
63*105f6285SAndroid Build Coastguard Worker
64*105f6285SAndroid Build Coastguard Worker  def __bool__(self):
65*105f6285SAndroid Build Coastguard Worker    return bool(self.added) or bool(self.removed) or bool(self.modified)
66*105f6285SAndroid Build Coastguard Worker
67*105f6285SAndroid Build Coastguard Worker
68*105f6285SAndroid Build Coastguard Workerdef element_string(e: Element) -> str:
69*105f6285SAndroid Build Coastguard Worker  return ET.tostring(e).decode(encoding='UTF-8').strip()
70*105f6285SAndroid Build Coastguard Worker
71*105f6285SAndroid Build Coastguard Worker
72*105f6285SAndroid Build Coastguard Workerdef attribute_changes(e1: Element, e2: Element,
73*105f6285SAndroid Build Coastguard Worker                      ignored_attrs: Set[str]) -> ChangeMap:
74*105f6285SAndroid Build Coastguard Worker  """Get the changes in attributes between two XML elements.
75*105f6285SAndroid Build Coastguard Worker
76*105f6285SAndroid Build Coastguard Worker  Arguments:
77*105f6285SAndroid Build Coastguard Worker    e1: the first xml element.
78*105f6285SAndroid Build Coastguard Worker    e2: the second xml element.
79*105f6285SAndroid Build Coastguard Worker    ignored_attrs: a set of attribute names to ignore changes.
80*105f6285SAndroid Build Coastguard Worker
81*105f6285SAndroid Build Coastguard Worker  Returns:
82*105f6285SAndroid Build Coastguard Worker    A ChangeMap of attribute changes. Keyed by attribute name.
83*105f6285SAndroid Build Coastguard Worker  """
84*105f6285SAndroid Build Coastguard Worker  changes = ChangeMap()
85*105f6285SAndroid Build Coastguard Worker  attributes = set(e1.keys()) | set(e2.keys())
86*105f6285SAndroid Build Coastguard Worker  for attr in attributes:
87*105f6285SAndroid Build Coastguard Worker    if attr in ignored_attrs:
88*105f6285SAndroid Build Coastguard Worker      continue
89*105f6285SAndroid Build Coastguard Worker    a1 = e1.get(attr)
90*105f6285SAndroid Build Coastguard Worker    a2 = e2.get(attr)
91*105f6285SAndroid Build Coastguard Worker    if a1 == a2:
92*105f6285SAndroid Build Coastguard Worker      continue
93*105f6285SAndroid Build Coastguard Worker    elif not a1:
94*105f6285SAndroid Build Coastguard Worker      changes.added[attr] = a2 or ''
95*105f6285SAndroid Build Coastguard Worker    elif not a2:
96*105f6285SAndroid Build Coastguard Worker      changes.removed[attr] = a1
97*105f6285SAndroid Build Coastguard Worker    else:
98*105f6285SAndroid Build Coastguard Worker      changes.modified[attr] = Change(value_from=a1, value_to=a2)
99*105f6285SAndroid Build Coastguard Worker  return changes
100*105f6285SAndroid Build Coastguard Worker
101*105f6285SAndroid Build Coastguard Worker
102*105f6285SAndroid Build Coastguard Workerdef compare_subelements(
103*105f6285SAndroid Build Coastguard Worker    tag: str,
104*105f6285SAndroid Build Coastguard Worker    p1: Element,
105*105f6285SAndroid Build Coastguard Worker    p2: Element,
106*105f6285SAndroid Build Coastguard Worker    ignored_attrs: Set[str],
107*105f6285SAndroid Build Coastguard Worker    key_fn: Callable[[Element], str],
108*105f6285SAndroid Build Coastguard Worker    diff_fn: Callable[[Element, Element, Set[str]], Any]) -> ChangeMap:
109*105f6285SAndroid Build Coastguard Worker  """Get the changes between subelements of two parent elements.
110*105f6285SAndroid Build Coastguard Worker
111*105f6285SAndroid Build Coastguard Worker  Arguments:
112*105f6285SAndroid Build Coastguard Worker    tag: tag name for children element.
113*105f6285SAndroid Build Coastguard Worker    p1: the base parent xml element.
114*105f6285SAndroid Build Coastguard Worker    p2: the parent xml element to compare
115*105f6285SAndroid Build Coastguard Worker    ignored_attrs: a set of attribute names to ignore changes.
116*105f6285SAndroid Build Coastguard Worker    key_fn: Function that takes a subelement and returns a key
117*105f6285SAndroid Build Coastguard Worker    diff_fn: Function that take two subelements and a set of ignored
118*105f6285SAndroid Build Coastguard Worker      attributes, returns the differences
119*105f6285SAndroid Build Coastguard Worker
120*105f6285SAndroid Build Coastguard Worker  Returns:
121*105f6285SAndroid Build Coastguard Worker    A ChangeMap object of the changes.
122*105f6285SAndroid Build Coastguard Worker  """
123*105f6285SAndroid Build Coastguard Worker  changes = ChangeMap()
124*105f6285SAndroid Build Coastguard Worker  group1 = {}
125*105f6285SAndroid Build Coastguard Worker  for e1 in p1.findall(tag):
126*105f6285SAndroid Build Coastguard Worker    group1[key_fn(e1)] = e1
127*105f6285SAndroid Build Coastguard Worker
128*105f6285SAndroid Build Coastguard Worker  for e2 in p2.findall(tag):
129*105f6285SAndroid Build Coastguard Worker    key = key_fn(e2)
130*105f6285SAndroid Build Coastguard Worker    e1 = group1.pop(key, None)
131*105f6285SAndroid Build Coastguard Worker    if e1 is None:
132*105f6285SAndroid Build Coastguard Worker      changes.added[key] = element_string(e2)
133*105f6285SAndroid Build Coastguard Worker    else:
134*105f6285SAndroid Build Coastguard Worker      echange = diff_fn(e1, e2, ignored_attrs)
135*105f6285SAndroid Build Coastguard Worker      if echange:
136*105f6285SAndroid Build Coastguard Worker        changes.modified[key] = echange
137*105f6285SAndroid Build Coastguard Worker
138*105f6285SAndroid Build Coastguard Worker  for name, e1 in group1.items():
139*105f6285SAndroid Build Coastguard Worker    changes.removed[name] = element_string(e1)
140*105f6285SAndroid Build Coastguard Worker
141*105f6285SAndroid Build Coastguard Worker  return changes
142