From 4a7e8b0d72ae9d7b8395f899732a1c40145c38fe Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 31 Mar 2015 17:12:42 +0100 Subject: WIP work to add g-ir-diff --- g-ir-diff | 121 ++++ giscanner/gircomparator.py | 1196 ++++++++++++++++++++++++++++++++++++++ tests/scanner/test_comparator.py | 114 ++++ 3 files changed, 1431 insertions(+) create mode 100755 g-ir-diff create mode 100644 giscanner/gircomparator.py create mode 100755 tests/scanner/test_comparator.py 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 , , 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 ('' + '' + '' + '' + '' + '%s' + '' + '') % 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( + '', + '', + [ + (GIRComparator.OUTPUT_FORWARDS_INCOMPATIBLE, + 'Alias ‘N.A’ has changed type from ‘GObject.Object’ to its ' + 'subtype ‘Gio.InputStream’.'), + ]) + self.assertErrors( + '', + '', + [ + (GIRComparator.OUTPUT_BACKWARDS_INCOMPATIBLE, + 'Alias ‘N.A’ has changed type from ‘Gio.InputStream’ to ' + '‘GObject.Object’.'), + ]) + + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1