summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Withnall <philip.withnall@collabora.co.uk>2015-03-31 17:12:42 +0100
committerPhilip Withnall <philip.withnall@collabora.co.uk>2015-04-01 12:23:41 +0100
commit4a7e8b0d72ae9d7b8395f899732a1c40145c38fe (patch)
tree242bfbaf2a2623afb5e4d43dd5325e6a0f9d14a2
parent2699b11503550bcfde7a31bf867e4cf780d3d5f9 (diff)
downloadgobject-introspection-wip/api-diff.tar.gz
WIP work to add g-ir-diffwip/api-diff
-rwxr-xr-xg-ir-diff121
-rw-r--r--giscanner/gircomparator.py1196
-rwxr-xr-xtests/scanner/test_comparator.py114
3 files changed, 1431 insertions, 0 deletions
diff --git a/g-ir-diff b/g-ir-diff
new file mode 100755
index 00000000..c27993c2
--- /dev/null
+++ b/g-ir-diff
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2015 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+import os
+import sys
+import __builtin__
+
+if os.name == 'nt':
+ datadir = os.path.join(os.path.dirname(__file__), '..', 'share')
+else:
+ datadir = "/opt/gnome3/build/share"
+
+__builtin__.__dict__['DATADIR'] = datadir
+
+if 'GI_SCANNER_DEBUG' in os.environ:
+ def on_exception(exctype, value, tb):
+ print("Caught exception: %r %r" % (exctype, value))
+ import pdb
+ pdb.pm()
+ sys.excepthook = on_exception
+
+srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None)
+if srcdir is not None:
+ path = srcdir
+else:
+ # This is a private directory, we don't want to pollute the global
+ # namespace.
+ if os.name == 'nt':
+ # Makes g-ir-scanner 'relocatable' at runtime on Windows.
+ path = os.path.join(os.path.dirname(__file__),
+ '..', 'lib', 'gobject-introspection')
+ else:
+ # TODO
+ path = os.path.join('/opt/gnome3/build/lib', 'gobject-introspection')
+sys.path.insert(0, path)
+
+import argparse
+from giscanner.girparser import GIRParser
+from giscanner.gircomparator import GIRComparator
+
+# Warning categories.
+WARNING_CATEGORIES = [
+ 'info',
+ 'backwards-compatibility',
+ 'forwards-compatibility',
+]
+
+
+if __name__ == '__main__':
+ # Parse command line arguments.
+ parser = argparse.ArgumentParser(
+ description='Comparing GIR APIs for stability')
+ parser.add_argument('old_file', type=str, help='Old GIR file')
+ parser.add_argument('new_file', type=str, help='New GIR file')
+ parser.add_argument('--warnings', dest='warnings', metavar='CATEGORY,…',
+ type=str,
+ help='Warning categories (%s)' %
+ ', '.join(WARNING_CATEGORIES))
+
+ args = parser.parse_args()
+
+ if not args.old_file or not args.new_file:
+ parser.print_help()
+ sys.exit(1)
+
+ if args.warnings is None:
+ # Enable all warnings by default
+ _enabled_warnings = WARNING_CATEGORIES
+ else:
+ _enabled_warnings = args.warnings.split(',')
+
+ enabled_warnings = []
+ i = 0
+ for category in _enabled_warnings:
+ if category not in WARNING_CATEGORIES:
+ parser.print_help()
+ sys.exit(1)
+
+ # TODO: this is really unneat
+ enabled_warnings.append(i)
+ i += 1
+
+ # Parse the two files.
+ old_parser = GIRParser()
+ new_parser = GIRParser()
+
+ try:
+ filename = args.old_file
+ old_parser.parse(args.old_file)
+ filename = args.new_file
+ new_parser.parse(args.new_file)
+ except Exception as e:
+ sys.stderr.write('Error parsing ‘%s’:\n' % filename)
+ sys.stderr.write(e)
+ sys.exit(1)
+
+ old_namespace = old_parser.get_namespace()
+ new_namespace = new_parser.get_namespace()
+
+ # Compare the interfaces.
+ comparator = GIRComparator(old_namespace, new_namespace, enabled_warnings)
+ out = comparator.compare()
+ comparator.print_output()
+ sys.exit(out)
diff --git a/giscanner/gircomparator.py b/giscanner/gircomparator.py
new file mode 100644
index 00000000..1e784534
--- /dev/null
+++ b/giscanner/gircomparator.py
@@ -0,0 +1,1196 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2015 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+import os
+import sys
+
+from . import ast
+from .girparser import GIRParser
+from .girwriter import COMPATIBLE_GIR_VERSION
+from .collections import OrderedDict
+
+
+_xdg_data_dirs = [x for x in os.environ.get('XDG_DATA_DIRS', '').split(os.pathsep)]
+_xdg_data_dirs.append(DATADIR)
+
+if os.name != 'nt':
+ _xdg_data_dirs.append('/usr/share')
+
+
+class GIRComparator(object):
+
+ # Output severity levels.
+ OUTPUT_INFO = 0
+ OUTPUT_FORWARDS_INCOMPATIBLE = 1
+ OUTPUT_BACKWARDS_INCOMPATIBLE = 2
+
+ # TODO
+ WARNING_CATEGORIES = [0, 1, 2]
+
+ def __init__(self, old_namespace, new_namespace,
+ enabled_warnings=WARNING_CATEGORIES):
+ self._old_namespace = old_namespace
+ self._old_namespaces = {} # includes from _old_namespace
+ self._new_namespace = new_namespace
+ self._new_namespaces = {} # includes from _new_namespace
+ self._output = []
+ self._enabled_warnings = enabled_warnings
+ self._includepaths = []
+
+ # Public API
+
+ def set_include_paths(self, paths):
+ self._includepaths = list(paths)
+
+ def print_output(self):
+ """
+ Print all the info, warning and error messages generated by the most
+ recent call to compare().
+ """
+ for (level, message) in self._output:
+ if not self._warning_enabled(level):
+ continue
+
+ formatted_level = self._format_level(level)
+ fd = self._get_fd_for_level(level)
+ fd.write('%s: %s\n' % (formatted_level, message))
+
+ def get_output(self):
+ """
+ Return all the info, warning and error messages generated by the most
+ recent call to compare().
+ """
+ out = []
+
+ for (level, message) in self._output:
+ if not self._warning_enabled(level):
+ continue
+
+ out.append((level, message))
+
+ return out
+
+ def _find_include(self, include):
+ searchdirs = self._includepaths[:]
+ for path in _xdg_data_dirs:
+ searchdirs.append(os.path.join(path, 'gir-1.0'))
+ searchdirs.append(os.path.join(DATADIR, 'gir-1.0'))
+
+ girname = '%s-%s.gir' % (include.name, include.version)
+ for d in searchdirs:
+ path = os.path.join(d, girname)
+ if os.path.exists(path):
+ return path
+ self._issue_output(self.OUTPUT_INFO,
+ 'Could not find include %r (search path: %r)' %
+ (girname, searchdirs))
+ return None
+
+ # Adds items to @namespaces and returns boolean success/fail.
+ def _parse_includes(self, namespace, namespaces):
+ for inc in namespace.includes:
+ # Already parsed it?
+ if inc.name in namespaces:
+ if namespaces[inc.name].version == inc.version:
+ continue
+
+ # Already parsed a different version.
+ self._issue_output(self.OUTPUT_INFO,
+ 'Incompatible versions of namespace '
+ '‘%s’ in includes: %s and %s.' %
+ (inc.name, namespaces[inc.name].version,
+ inc.version))
+ return False # bail immediately
+
+ # Parse the namespace.
+ try:
+ parser = GIRParser(types_only=True)
+ filename = self._find_include(inc)
+ if filename is None:
+ return False # bail immediately
+ parser.parse(filename)
+ parsed_namespace = parser.get_namespace()
+ namespaces[parsed_namespace.name] = parsed_namespace
+ except Exception as e:
+ print(e)
+ self._issue_output(self.OUTPUT_INFO,
+ 'Failed to parse included namespace '
+ '‘%s-%s’.' %
+ (inc.name, inc.version))
+ return False # bail immediately
+
+ # Parse its includes.
+ if not self._parse_includes(parsed_namespace, namespaces):
+ return False
+
+ return True
+
+ def compare(self):
+ """
+ Compare the two interfaces and store the results. Return 0 if no
+ relevant warnings were outputted; a positive integer otherwise. The
+ return value is affected by the categories of enabled warnings.
+ """
+ self._output = []
+
+ # TODO: do <include>, <package>, <c:include> affect API?
+
+ if self._old_namespace.name != self._new_namespace.name or \
+ self._old_namespace.version != self._new_namespace.version:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Namespace has changed name from ‘%s-%s’ to '
+ '‘%s-%s’.' %
+ (self._old_namespace.name,
+ self._old_namespace.version,
+ self._new_namespace.name,
+ self._new_namespace.version))
+
+ # Sufficiently big difference to give up on comparison right here.
+ return 1
+
+ # Parse the transitive closure of included namespaces so that types can
+ # be resolved for subtype analysis.
+ self._old_namespaces = {}
+ self._new_namespaces = {}
+ if not self._parse_includes(self._old_namespace,
+ self._old_namespaces) or \
+ not self._parse_includes(self._new_namespace,
+ self._new_namespaces):
+ # Sufficiently big failure to give up right now.
+ return 1
+
+ # TODO: what about shared-library, identifier-prefixes,
+ # symbol-prefixes?
+ for (name, node) in self._old_namespace.names.iteritems():
+ # See if the old node exists in the new file.
+ if name not in self._new_namespace.names:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Node ‘%s’ has been removed.' %
+ self._format_name(node))
+ else:
+ # Compare the two.
+ self._compare_nodes(node, self._new_namespace.names[name])
+
+ for (name, node) in self._new_namespace.names.iteritems():
+ # See if the new node exists in the old file.
+ if name not in self._old_namespace.names:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Node ‘%s’ has been added.' %
+ self._format_name(node))
+
+ # Work out the exit status.
+ retval = 0
+
+ for (level, _) in self._output:
+ if level != self.OUTPUT_INFO and self._warning_enabled(level):
+ retval = 1
+
+ return retval
+
+ # Private
+
+ # TODO: Support source code locations, and unique error codes for lookup
+ # online.
+ def _issue_output(self, level, message):
+ self._output.append((level, message))
+
+ def _format_level(self, level):
+ return [' INFO', ' WARN', 'ERROR'][level]
+
+ def _get_fd_for_level(self, level):
+ if level == self.OUTPUT_INFO:
+ return sys.stdout
+ return sys.stderr
+
+ def _warning_enabled(self, level):
+ return level in self._enabled_warnings
+
+ def _get_type_for_node(self, node):
+ return node.namespace.type_from_name(node.name)
+
+ def _get_node_for_type(self, type, namespaces):
+ if type is None:
+ return None
+
+ assert type.target_giname is not None
+
+ [namespace, name] = type.target_giname.split('.')
+ if namespace in namespaces and \
+ name in namespaces[namespace].names:
+ return namespaces[namespace].names[name]
+
+ # Failure.
+ self._issue_output(self.OUTPUT_INFO,
+ 'Could not resolve type ‘%s’ to node.' % type)
+ return None
+
+ # Returns (gpointer > type). NOTE: This is strict subtyping.
+ def _type_is_gpointer_subtype(self, type):
+ # TODO
+ return False
+
+ TYPES_NONEQUAL = 0 # ¬(A :> B) ∧ ¬(B :> A)
+ TYPES_EQUAL = 1 # (A :> B) ∧ (B :> A)
+ TYPES_SUPERTYPE = 2 # (A :> B) ∧ ¬(B :> A)
+ TYPES_SUBTYPE = 3 # ¬(A :> B) ∧ (B :> A)
+
+ # Returns (A :> B)
+ def _type_is_supertype(self, type_a, node_a, type_b, node_b, namespaces_b):
+ while node_b is not None and not type_a.is_equiv(type_b):
+ type_b = node_b.parent_type
+ node_b = self._get_node_for_type(type_b, namespaces_b)
+ return node_b is not None
+
+ def _types_equal(self, type_a, type_b):
+ # Allow None as an alias for 'no parent type'.
+ if (type_a is None and type_b is None) or type_a.is_equiv(type_b):
+ return self.TYPES_EQUAL
+
+ # If the types aren't resolved, bail immediately.
+ if not type_a.resolved or not type_b.resolved:
+ self._issue_output(self.OUTPUT_INFO,
+ 'Could not resolve types ‘%s’ and ‘%s’ for '
+ 'comparison.' % (type_a, type_b))
+ return self.TYPES_NONEQUAL
+
+ # Special case gpointers, as it's common for an annotation to be added
+ # which 'subtypes' from gpointer to a more specific type.
+ if type_a.target_fundamental == 'gpointer' and \
+ self._type_is_gpointer_subtype(type_b):
+ return self.TYPES_SUPERTYPE
+ elif (type_b.target_fundamental == 'gpointer' and
+ self._type_is_gpointer_subtype(type_a)):
+ return self.TYPES_SUBTYPE
+
+ # Otherwise, if they are fundamental or foreign types, bail silently.
+ if type_a.target_giname is None or type_b.target_giname is None:
+ return self.TYPES_NONEQUAL
+
+ # Check for subtype relations. That means looking up the Node instances
+ # for the given types, which may come from other namespaces.
+ node_a = self._get_node_for_type(type_a, self._old_namespaces)
+ node_b = self._get_node_for_type(type_b, self._new_namespaces)
+
+ if ((isinstance(node_a, ast.Class) and
+ isinstance(node_b, ast.Class)) or
+ (isinstance(node_a, ast.Interface) and
+ isinstance(node_b, ast.Interface))):
+ if self._type_is_supertype(type_a, node_a,
+ type_b, node_b, self._new_namespaces):
+ return self.TYPES_SUPERTYPE
+ elif self._type_is_supertype(type_b, node_b,
+ type_a, node_a, self._old_namespaces):
+ return self.TYPES_SUBTYPE
+
+ return self.TYPES_NONEQUAL
+
+ def _compare_nodes(self, old_node, new_node):
+ # TODO:
+ # ast.Include, ast.ErrorQuarkFunction,
+ # ast.Field, ast.Union, ast.Boxed,
+ # foreign
+
+ # Precondition of calling this function.
+ assert old_node.name == new_node.name
+ assert old_node.namespace.name == new_node.namespace.name
+ assert old_node.namespace.version == new_node.namespace.version
+
+ if type(old_node) != type(new_node):
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Node ‘%s’ has changed type from %s to %s.' %
+ (self._format_name(old_node),
+ type(old_node), type(new_node)))
+
+ # Delegate to type-specific comparison functions.
+ comparison_func_map = {
+ ast.Function: self._compare_functions,
+ ast.Class: self._compare_classes,
+ ast.Record: self._compare_records,
+ ast.Constant: self._compare_constants,
+ ast.Enum: self._compare_enums,
+ ast.Bitfield: self._compare_bitfields,
+ ast.Interface: self._compare_interfaces,
+ ast.Alias: self._compare_aliases,
+ ast.Callback: self._compare_callbacks,
+ ast.Union: self._compare_unions,
+ }
+
+ if type(old_node) in comparison_func_map:
+ comparison_func_map[type(old_node)](old_node, new_node)
+ else:
+ self._issue_output(self.OUTPUT_INFO,
+ 'No comparison function for node ‘%s’ of type '
+ '%s.' %
+ (self._format_name(old_node),
+ type(old_node)))
+
+ def _compare_callables(self, old_callable, new_callable):
+ # Precondition of calling this function.
+ assert old_callable.parent.name == new_callable.parent.name
+
+ # Compare instance parameters.
+ if (old_callable.instance_parameter is None) != \
+ (new_callable.instance_parameter is None):
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Callable ‘%s’ has changed instance '
+ 'parameter from %s to %s. TODO' %
+ (self._format_name(old_callable),
+ old_callable.instance_parameter,
+ new_callable.instance_parameter))
+ elif old_callable.instance_parameter is not None:
+ self._compare_parameters(old_callable.instance_parameter,
+ new_callable.instance_parameter,
+ old_callable, new_callable)
+
+ # Compare normal parameters.
+ n_old_params = len(old_callable.parameters)
+ n_new_params = len(new_callable.parameters)
+
+ for i in range(max(n_old_params, n_new_params)):
+ if i >= n_old_params:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Parameter %u (‘%s’) of callable ‘%s’ '
+ 'has been added.' %
+ (i, new_callable.parameters[i].name,
+ self._format_name(new_callable)))
+ elif i >= n_new_params:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Parameter %u (‘%s’) of callable ‘%s’ '
+ 'has been removed.' %
+ (i, old_callable.parameters[i].name,
+ self._format_name(old_callable)))
+ else:
+ self._compare_parameters(old_callable.parameters[i],
+ new_callable.parameters[i],
+ old_callable, new_callable)
+
+ # Throwing errors.
+ if old_callable.throws and not new_callable.throws:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Callable ‘%s’ no longer throws '
+ 'exceptions.' %
+ self._format_name(new_callable))
+ elif not old_callable.throws and new_callable.throws:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Callable ‘%s’ now throws exceptions.' %
+ self._format_name(new_callable))
+
+ # Return value.
+ type_comparison = self._types_equal(old_callable.retval.type,
+ new_callable.retval.type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Return value of callable ‘%s’ has changed '
+ 'type from ‘%s’ to its subtype ‘%s’.' %
+ (self._format_name(new_callable),
+ old_callable.retval.type,
+ new_callable.retval.type))
+ elif type_comparison != self.TYPES_EQUAL:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Return value of callable ‘%s’ has changed '
+ 'type from ‘%s’ to ‘%s’.' %
+ (self._format_name(new_callable),
+ old_callable.retval.type,
+ new_callable.retval.type))
+ if old_callable.retval.nullable and not new_callable.retval.nullable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Return value of callable ‘%s’ is no longer '
+ 'nullable.' %
+ self._format_name(new_callable))
+ elif not old_callable.retval.nullable and new_callable.retval.nullable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Return value of callable ‘%s’ is now '
+ 'nullable.' %
+ self._format_name(new_callable))
+ if old_callable.retval.transfer != new_callable.retval.transfer:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Return value of callable ‘%s’ has changed '
+ 'transfer from ‘%s’ to ‘%s’.' %
+ (self._format_name(new_callable),
+ old_callable.retval.transfer,
+ new_callable.retval.transfer))
+
+ def _compare_functions(self, old_function, new_function):
+ self._compare_callables(old_function, new_function)
+
+ # TODO: symbol, is_constructor, shadowed_by, shadows,
+ # moved_to, internal_skipped
+ if old_function.is_method and not new_function.is_method:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Function ‘%s’ has changed from a method to a '
+ 'function.' % self._format_name(old_function))
+ elif not old_function.is_method and new_function.is_method:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Function ‘%s’ has changed from a function to '
+ 'a method.' % self._format_name(old_function))
+
+ def _compare_virtual_functions(self, old_function, new_function):
+ self._compare_functions(old_function, new_function)
+
+ def _compare_properties(self, old_property, new_property):
+ # Precondition of calling this function.
+ assert old_property.name == new_property.name
+ assert old_property.parent.name == new_property.parent.name
+
+ # Compare types.
+ type_comparison = self._types_equal(old_property.type,
+ new_property.type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ # new_property.type is a subtype of old_property.type
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has changed type from ‘%s’ '
+ 'to its subtype ‘%s’.' %
+ (self._format_name(old_property),
+ old_property.type, new_property.type))
+ elif type_comparison != self.TYPES_EQUAL:
+ # The types are in a supertype relationship in the wrong
+ # direction, or not related at all
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has changed type from ‘%s’ '
+ 'to ‘%s’.' %
+ (self._format_name(old_property),
+ old_property.type, new_property.type))
+
+ # Readability and writeability.
+ if old_property.readable and not new_property.readable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has become non-readable.' %
+ self._format_name(old_property))
+ elif not old_property.readable and new_property.readable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has become readable.' %
+ self._format_name(old_property))
+
+ if old_property.writable and not new_property.writable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has become non-writable.' %
+ self._format_name(old_property))
+ elif not old_property.writable and new_property.writable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has become writable.' %
+ self._format_name(old_property))
+
+ # Constructability. .construct is True if the property must be set at
+ # construction time.
+ if old_property.construct and not new_property.construct:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ no longer has to be set on '
+ 'construction.' %
+ self._format_name(old_property))
+ elif not old_property.construct and new_property.construct:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ now has to be set on '
+ 'construction.' %
+ self._format_name(old_property))
+
+ if old_property.construct_only and not new_property.construct_only:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ can now be set after '
+ 'construction.' %
+ self._format_name(old_property))
+ elif not old_property.construct_only and new_property.construct_only:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ can now only be set on '
+ 'construction.' %
+ self._format_name(old_property))
+
+ # Transfer
+ if old_property.transfer != new_property.transfer:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has changed transfer from '
+ '‘%s’ to ‘%s’.' %
+ (self._format_name(old_property),
+ old_property.transfer, new_property.transfer))
+
+ def _compare_signals(self, old_signal, new_signal):
+ # TODO: when, no_recurse, detailed, action, no_hooks
+
+ # Precondition for calling this function.
+ assert old_signal.name == new_signal.name
+
+ self._compare_callables(old_signal, new_signal)
+
+ def _compare_fields(self, old_field, new_field):
+ # TODO: Annotated, bits, anonymous_node
+
+ # Precondition of calling this function.
+ assert old_field.name == new_field.name
+ assert old_field.parent.name == new_field.parent.name
+ assert old_field.namespace.name == new_field.namespace.name
+
+ # Compare types.
+ type_comparison = self._types_equal(old_field.type,
+ new_field.type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ # new_field.type is a subtype of old_field.type
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has changed type from ‘%s’ '
+ 'to its subtype ‘%s’.' %
+ (self._format_name(old_field),
+ old_field.type, new_field.type))
+ elif type_comparison != self.TYPES_EQUAL:
+ # The types are in a supertype relationship in the wrong
+ # direction, or not related at all
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has changed type from ‘%s’ '
+ 'to ‘%s’.' %
+ (self._format_name(old_field),
+ old_field.type, new_field.type))
+
+ # Readability and writeability.
+ if old_field.readable and not new_field.readable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become non-readable.' %
+ self._format_name(old_field))
+ elif not old_field.readable and new_field.readable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become readable.' %
+ self._format_name(old_field))
+
+ if old_field.writable and not new_field.writable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become non-writable.' %
+ self._format_name(old_field))
+ elif not old_field.writable and new_field.writable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become writable.' %
+ self._format_name(old_field))
+
+ # Privacy.
+ if not old_field.private and new_field.private:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become private.' %
+ self._format_name(old_field))
+ elif old_field.private and not new_field.private:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has become public.' %
+ self._format_name(old_field))
+
+ def _compare_parameters(self, old_parameter, new_parameter,
+ old_parent=None, new_parent=None):
+ # TODO: TypeContainer, closure_name, destroy_name, scope,
+ # caller_allocates
+
+ if old_parameter.argname != new_parameter.argname:
+ self._issue_output(self.OUTPUT_INFO,
+ 'Parameter has changed name from ‘%s’ to '
+ '‘%s’.' %
+ (self._format_name(old_parameter, old_parent),
+ self._format_name(new_parameter, new_parent)))
+
+ # TODO: Are direction changes from * to inout OK?
+ if old_parameter.direction != new_parameter.direction:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Parameter ‘%s’ has changed direction from '
+ '‘%s’ to ‘%s’.' %
+ (self._format_name(old_parameter, old_parent),
+ old_parameter.direction,
+ new_parameter.direction))
+
+ # Optionality.
+ if old_parameter.optional and not new_parameter.optional:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Parameter ‘%s’ has become mandatory.' %
+ self._format_name(old_parameter, old_parent))
+ elif not old_parameter.optional and new_parameter.optional:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Parameter ‘%s’ has become optional.' %
+ self._format_name(old_parameter, old_parent))
+
+ # Nullability.
+ if old_parameter.nullable and not new_parameter.nullable:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Parameter ‘%s’ has become non-nullable.' %
+ self._format_name(old_parameter, old_parent))
+ elif not old_parameter.nullable and new_parameter.nullable:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Parameter ‘%s’ has become nullable.' %
+ self._format_name(old_parameter, old_parent))
+
+ def _compare_classes(self, old_class, new_class):
+ # TODO: ctype, c_symbol_prefix, parent_type, fundamental, unref_func,
+ # ref_func, set_value_func, get_value_func, parent_chain,
+ # glib_type_struct,
+
+ # Precondition of calling this function.
+ assert old_class.name == new_class.name
+
+ if old_class.is_abstract and not new_class.is_abstract:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Class ‘%s’ has become non-abstract.' %
+ self._format_name(old_class))
+ elif not old_class.is_abstract and new_class.is_abstract:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Class ‘%s’ has become abstract.' %
+ self._format_name(old_class))
+
+ type_comparison = self._types_equal(old_class.parent_type,
+ new_class.parent_type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ # new_class.parent_type is a subtype of old_class.parent_type
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Class ‘%s’ has changed parent type from ‘%s’ '
+ 'to its subtype ‘%s’.' %
+ (self._format_name(old_class),
+ old_class.parent_type, new_class.parent_type))
+ elif type_comparison != self.TYPES_EQUAL:
+ # The parent types are in a supertype relationship in the wrong
+ # direction, or not related at all
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Class ‘%s’ has changed parent type from ‘%s’ '
+ 'to ‘%s’.' %
+ (self._format_name(old_class),
+ old_class.parent_type, new_class.parent_type))
+
+ # Methods.
+ (a, both, b) = self._lists_equal(old_class.methods,
+ new_class.methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Virtual methods.
+ (a, both, b) = self._lists_equal(old_class.virtual_methods,
+ new_class.virtual_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Virtual method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Virtual method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_virtual_functions(old_method, new_method)
+
+ # Static methods.
+ (a, both, b) = self._lists_equal(old_class.static_methods,
+ new_class.static_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Constructors.
+ (a, both, b) = self._lists_equal(old_class.constructors,
+ new_class.constructors,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Interfaces. NOTE: We do not compare interfaces in @both for equality,
+ # since they're just #Types.
+ (a, both, b) = self._lists_equal(old_class.interfaces,
+ new_class.interfaces,
+ lambda x: str(x))
+ for old_type in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Interface implementation ‘%s’ has been '
+ 'removed from class ‘%s’.' %
+ (old_type, self._format_name(old_class)))
+ for new_type in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Interface implementation ‘%s’ has been '
+ 'added to class ‘%s’.' %
+ (new_type, self._format_name(new_class)))
+
+ # Properties.
+ (a, both, b) = self._lists_equal(old_class.properties,
+ new_class.properties,
+ lambda x: x.name)
+ for old_property in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has been removed.' %
+ self._format_name(old_property))
+ for new_property in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has been added.' %
+ self._format_name(new_property))
+ for (old_property, new_property) in both:
+ self._compare_properties(old_property, new_property)
+
+ # Signals.
+ (a, both, b) = self._lists_equal(old_class.signals,
+ new_class.signals,
+ lambda x: x.name)
+ for old_signal in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Signal ‘%s’ has been removed.' %
+ self._format_name(old_signal))
+ for new_signal in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Signal ‘%s’ has been added.' %
+ self._format_name(new_signal))
+ for (old_signal, new_signal) in both:
+ self._compare_signals(old_signal, new_signal)
+
+ # Fields.
+ (a, both, b) = self._lists_equal(old_class.fields, new_class.fields,
+ lambda x: x.name)
+ for old_field in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been removed.' %
+ self._format_name(old_field))
+ for new_field in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been added.' %
+ self._format_name(new_field))
+ for (old_field, new_field) in both:
+ self._compare_fields(old_field, new_field)
+
+ def _compare_compounds(self, old_compound, new_compound):
+ # TODO: Registered, ctype, disguised, gtype_name, get_type,
+ # c_symbol_prefix, tag_name
+
+ # Methods.
+ (a, both, b) = self._lists_equal(old_compound.methods,
+ new_compound.methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Static methods.
+ (a, both, b) = self._lists_equal(old_compound.static_methods,
+ new_compound.static_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Constructors.
+ (a, both, b) = self._lists_equal(old_compound.constructors,
+ new_compound.constructors,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Fields.
+ (a, both, b) = self._lists_equal(old_compound.fields,
+ new_compound.fields,
+ lambda x: x.name)
+ for old_field in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been removed.' %
+ self._format_name(old_field))
+ for new_field in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been added.' %
+ self._format_name(new_field))
+ for (old_field, new_field) in both:
+ self._compare_fields(old_field, new_field)
+
+ def _compare_records(self, old_record, new_record):
+ # TODO: is_gtype_struct_for
+ self._compare_compounds(old_record, new_record)
+
+ def _compare_constants(self, old_constant, new_constant):
+ # TODO: ctype
+
+ # Check the types.
+ type_comparison = self._types_equal(old_constant.value_type,
+ new_constant.value_type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Constant ‘%s’ has changed type from ‘%s’ to '
+ 'its subtype ‘%s’.' %
+ (self._format_name(new_constant),
+ old_constant.value_type,
+ new_constant.value_type))
+ elif type_comparison != self.TYPES_EQUAL:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Constant ‘%s’ has changed type from ‘%s’ to '
+ '‘%s’.' %
+ (self._format_name(new_constant),
+ old_constant.value_type,
+ new_constant.value_type))
+
+ # Compare the values for information only.
+ if old_constant.value != new_constant.value:
+ self._issue_output(self.OUTPUT_INFO,
+ 'Constant ‘%s’ has changed value from ‘%s’ to '
+ '‘%s’.' %
+ (self._format_name(new_constant),
+ old_constant.value,
+ new_constant.value))
+
+ def _compare_enums(self, old_enum, new_enum):
+ # TODO: Registered, ctype, c_symbol_prefix
+
+ # Members.
+ (a, both, b) = self._lists_equal(old_enum.members,
+ new_enum.members,
+ lambda x: x.name)
+ for old_member in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Member ‘%s’ has been removed.' %
+ self._format_name(old_member))
+ for new_member in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Member ‘%s’ has been added.' %
+ self._format_name(new_member))
+ for (old_member, new_member) in both:
+ self._compare_members(old_member, new_member)
+
+ # Static methods.
+ (a, both, b) = self._lists_equal(old_enum.static_methods,
+ new_enum.static_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ def _compare_bitfields(self, old_bitfield, new_bitfield):
+ # TODO: Registered, ctype, c_symbol_prefix
+
+ # Members.
+ (a, both, b) = self._lists_equal(old_bitfield.members,
+ new_bitfield.members,
+ lambda x: x.name)
+ for old_member in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Member ‘%s’ has been removed.' %
+ self._format_name(old_member))
+ for new_member in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Member ‘%s’ has been added.' %
+ self._format_name(new_member))
+ for (old_member, new_member) in both:
+ self._compare_members(old_member, new_member)
+
+ # Static methods.
+ (a, both, b) = self._lists_equal(old_bitfield.static_methods,
+ new_bitfield.static_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ def _compare_members(self, old_member, new_member):
+ # TODO: Annotated, symbol, nick
+
+ # Precondition of calling this function.
+ assert old_member.name == new_member.name
+ assert old_member.parent.name == new_member.parent.name
+
+ # Information output.
+ if old_member.value != new_member.value:
+ self._issue_output(self.OUTPUT_INFO,
+ 'Member ‘%s’ has changed value from ‘%s’ to '
+ '‘%s’.' %
+ (self._format_name(old_member),
+ old_member.value, new_member.value))
+
+ def _compare_interfaces(self, old_interface, new_interface):
+ # TODO: Registered, ctype, c_symbol_prefix, glib_type_struct
+
+ # Precondition of calling this function.
+ assert old_interface.name == new_interface.name
+
+ # Parent type.
+ type_comparison = self._types_equal(old_interface.parent_type,
+ new_interface.parent_type)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ # new_interface.parent_type is a subtype of
+ # old_interface.parent_type
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Interface ‘%s’ has changed parent type from '
+ '‘%s’ to its subtype ‘%s’.' %
+ (self._format_name(old_interface),
+ old_interface.parent_type,
+ new_interface.parent_type))
+ elif type_comparison != self.TYPES_EQUAL:
+ # The parent types are in a supertype relationship in the wrong
+ # direction, or not related at all
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Interface ‘%s’ has changed parent type from '
+ '‘%s’ to ‘%s’.' %
+ (self._format_name(old_interface),
+ old_interface.parent_type,
+ new_interface.parent_type))
+
+ # Prerequisite interfaces. NOTE: We do not compare interfaces in @both
+ # for equality, since they're just #Types.
+ (a, both, b) = self._lists_equal(old_interface.prerequisites,
+ new_interface.prerequisites,
+ lambda x: str(x))
+ for old_type in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Prerequisite ‘%s’ has been removed from '
+ 'interface ‘%s’.' %
+ (old_type, self._format_name(old_interface)))
+ for new_type in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Prerequisite ‘%s’ has been added to '
+ 'interface ‘%s’.' %
+ (new_type, self._format_name(new_interface)))
+
+ # Methods.
+ (a, both, b) = self._lists_equal(old_interface.methods,
+ new_interface.methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Virtual methods.
+ (a, both, b) = self._lists_equal(old_interface.virtual_methods,
+ new_interface.virtual_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Virtual method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Virtual method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_virtual_functions(old_method, new_method)
+
+ # Static methods.
+ (a, both, b) = self._lists_equal(old_interface.static_methods,
+ new_interface.static_methods,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Static method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Constructors.
+ (a, both, b) = self._lists_equal(old_interface.constructors,
+ new_interface.constructors,
+ lambda x: x.name)
+ for old_method in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been removed.' %
+ self._format_name(old_method))
+ for new_method in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Constructor method ‘%s’ has been added.' %
+ self._format_name(new_method))
+ for (old_method, new_method) in both:
+ self._compare_functions(old_method, new_method)
+
+ # Properties.
+ (a, both, b) = self._lists_equal(old_interface.properties,
+ new_interface.properties,
+ lambda x: x.name)
+ for old_property in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has been removed.' %
+ self._format_name(old_property))
+ for new_property in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Property ‘%s’ has been added.' %
+ self._format_name(new_property))
+ for (old_property, new_property) in both:
+ self._compare_properties(old_property, new_property)
+
+ # Signals.
+ (a, both, b) = self._lists_equal(old_interface.signals,
+ new_interface.signals,
+ lambda x: x.name)
+ for old_signal in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Signal ‘%s’ has been removed.' %
+ self._format_name(old_signal))
+ for new_signal in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Signal ‘%s’ has been added.' %
+ self._format_name(new_signal))
+ for (old_signal, new_signal) in both:
+ self._compare_signals(old_signal, new_signal)
+
+ # Fields.
+ (a, both, b) = self._lists_equal(old_interface.fields,
+ new_interface.fields,
+ lambda x: x.name)
+ for old_field in a:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been removed.' %
+ self._format_name(old_field))
+ for new_field in b:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Field ‘%s’ has been added.' %
+ self._format_name(new_field))
+ for (old_field, new_field) in both:
+ self._compare_fields(old_field, new_field)
+
+ def _compare_aliases(self, old_alias, new_alias):
+ # TODO: ctype
+
+ # Precondition of calling this function.
+ assert old_alias.name == new_alias.name
+
+ type_comparison = self._types_equal(old_alias.target,
+ new_alias.target)
+ if type_comparison == self.TYPES_SUPERTYPE:
+ self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Alias ‘%s’ has changed type from ‘%s’ to '
+ 'its subtype ‘%s’.' %
+ (self._format_name(new_alias),
+ old_alias.target, new_alias.target))
+ elif type_comparison != self.TYPES_EQUAL:
+ self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Alias ‘%s’ has changed type from ‘%s’ to '
+ '‘%s’.' %
+ (self._format_name(new_alias),
+ old_alias.target, new_alias.target))
+
+ def _compare_callbacks(self, old_callback, new_callback):
+ # TODO: ctype
+
+ # Precondition of calling this function.
+ assert old_callback.name == new_callback.name
+
+ self._compare_callables(old_callback, new_callback)
+
+ def _compare_unions(self, old_union, new_union):
+ self._compare_compounds(old_union, new_union)
+
+ def _lists_equal(self, list_a, list_b, id_func):
+ """
+ Compare two lists of objects using the IDs returned by calling @id_func
+ on them. Return a tuple
+ (elements_in_a_only, elements_in_both, elements_in_b_only).
+ elements_in_both contains tuples of the element in list A and in
+ list B.
+
+ This assumes that the objects in the lists are of the same type, and
+ that the ID returned by @id_func is stable for comparisons.
+ """
+ # Convert the two to dictionaries for fast comparisons.
+ dict_a = {}
+ dict_b = {}
+
+ for obj in list_a:
+ dict_a[id_func(obj)] = obj
+ for obj in list_b:
+ dict_b[id_func(obj)] = obj
+
+ # Output
+ objs_in_a_only = []
+ objs_in_both = []
+ objs_in_b_only = []
+
+ for obj in list_a:
+ if id_func(obj) in dict_b:
+ objs_in_both.append((obj, dict_b[id_func(obj)]))
+ else:
+ objs_in_a_only.append(obj)
+ for obj in list_b:
+ if id_func(obj) not in dict_a:
+ objs_in_b_only.append(obj)
+
+ return (objs_in_a_only, objs_in_both, objs_in_b_only)
+
+ # FIXME: parent should be replaced by the proper parenting infrastructure
+ # in ast.Annotated or similar
+ def _format_name(self, node, parent=None):
+ if isinstance(node, ast.Node) or isinstance(node, ast.Member):
+ output = node.name
+ while True:
+ node = node.parent
+ if isinstance(node, ast.Namespace):
+ break
+ output = node.name + '.' + output
+ output = node.name + '.' + output # namespace
+ elif isinstance(node, ast.Parameter):
+ output = node.argname
+ if parent is not None:
+ output = self._format_name(parent) + '.' + output
+ else:
+ output = node.name
+
+ return output
diff --git a/tests/scanner/test_comparator.py b/tests/scanner/test_comparator.py
new file mode 100755
index 00000000..b6b2649e
--- /dev/null
+++ b/tests/scanner/test_comparator.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# TODO: copyright
+
+import unittest
+import tempfile
+import os
+import sys
+import __builtin__
+
+
+os.environ['GI_SCANNER_DISABLE_CACHE'] = '1'
+path = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None)
+assert path is not None
+sys.path.insert(0, path)
+
+# Not correct, but enough to get the tests going uninstalled
+__builtin__.__dict__['DATADIR'] = path
+
+from giscanner.girparser import GIRParser
+from giscanner.gircomparator import GIRComparator
+from giscanner.message import MessageLogger, WARNING, ERROR, FATAL
+
+
+class TestComparatorErrors(unittest.TestCase):
+ def _create_temp_gir_file(self, xml):
+ tmp_fd, tmp_name = tempfile.mkstemp(suffix='.gir', text=True)
+ file = os.fdopen(tmp_fd, 'wt')
+ file.write(xml)
+ file.close()
+
+ return tmp_name
+
+ def _test_comparator(self, old_xml, new_xml, wrap=True):
+ # Wrap the files in a repository and namespace for convenience.
+ if wrap:
+ old_xml = self._wrap_gir(old_xml)
+ new_xml = self._wrap_gir(new_xml)
+
+ old_tmpfile = self._create_temp_gir_file(old_xml)
+ new_tmpfile = self._create_temp_gir_file(new_xml)
+
+ old_parser = GIRParser()
+ new_parser = GIRParser()
+
+ old_parser.parse(old_tmpfile)
+ new_parser.parse(new_tmpfile)
+
+ old_namespace = old_parser.get_namespace()
+ new_namespace = new_parser.get_namespace()
+
+ os.unlink(new_tmpfile)
+ os.unlink(old_tmpfile)
+
+ self.assertNotEqual(old_namespace, None)
+ self.assertNotEqual(new_namespace, None)
+
+ return GIRComparator(old_namespace, new_namespace)
+
+ def _wrap_gir(self, xml):
+ return ('<?xml version="1.0"?>'
+ '<repository version="1.2" '
+ 'xmlns="http://www.gtk.org/introspection/core/1.0" '
+ 'xmlns:c="http://www.gtk.org/introspection/c/1.0">'
+ '<include name="GObject" version="2.0"/>'
+ '<include name="Gio" version="2.0"/>'
+ '<namespace name="N" '
+ 'version="1.0" '
+ 'shared-library="libtest-1.0.so.0" '
+ 'c:identifier-prefixes="Test" '
+ 'c:symbol-prefixes="test">'
+ '%s'
+ '</namespace>'
+ '</repository>') % xml
+
+ def assertSuccess(self, old_xml, new_xml, wrap=True):
+ comparator = self._test_comparator(old_xml, new_xml, wrap)
+ self.assertEqual(comparator.compare(), 0)
+
+ def assertErrors(self, old_xml, new_xml, errors, wrap=True):
+ comparator = self._test_comparator(old_xml, new_xml, wrap)
+ self.assertNotEqual(comparator.compare(), 0)
+
+ output = comparator.get_output()
+ self.assertEqual(output, errors)
+
+ def assertInfos(self, old_xml, new_xml, infos, wrap=True):
+ comparator = self._test_comparator(old_xml, new_xml, wrap)
+ self.assertEqual(comparator.compare(), 0)
+
+ output = comparator.get_output()
+ self.assertEqual(output, infos)
+
+ def test_alias_type_changed(self):
+ self.assertErrors(
+ '<alias name="A"><type name="GObject.Object"/></alias>',
+ '<alias name="A"><type name="Gio.InputStream"/></alias>',
+ [
+ (GIRComparator.OUTPUT_FORWARDS_INCOMPATIBLE,
+ 'Alias ‘N.A’ has changed type from ‘GObject.Object’ to its '
+ 'subtype ‘Gio.InputStream’.'),
+ ])
+ self.assertErrors(
+ '<alias name="A"><type name="Gio.InputStream"/></alias>',
+ '<alias name="A"><type name="GObject.Object"/></alias>',
+ [
+ (GIRComparator.OUTPUT_BACKWARDS_INCOMPATIBLE,
+ 'Alias ‘N.A’ has changed type from ‘Gio.InputStream’ to '
+ '‘GObject.Object’.'),
+ ])
+
+
+if __name__ == '__main__':
+ unittest.main()