summaryrefslogtreecommitdiff
path: root/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
diff options
context:
space:
mode:
Diffstat (limited to 'utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py')
-rw-r--r--utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py397
1 files changed, 397 insertions, 0 deletions
diff --git a/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py b/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
new file mode 100644
index 00000000..a0ee150e
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
@@ -0,0 +1,397 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from mojom_parser_test_case import MojomParserTestCase
+
+
+class VersionCompatibilityTest(MojomParserTestCase):
+ """Tests covering compatibility between two versions of the same mojom type
+ definition. This coverage ensures that we can reliably detect unsafe changes
+ to definitions that are expected to tolerate version skew in production
+ environments."""
+
+ def _GetTypeCompatibilityMap(self, old_mojom, new_mojom):
+ """Helper to support the implementation of assertBackwardCompatible and
+ assertNotBackwardCompatible."""
+
+ old = self.ExtractTypes(old_mojom)
+ new = self.ExtractTypes(new_mojom)
+ self.assertEqual(set(old.keys()), set(new.keys()),
+ 'Old and new test mojoms should use the same type names.')
+
+ compatibility_map = {}
+ for name in old.keys():
+ compatibility_map[name] = new[name].IsBackwardCompatible(old[name])
+ return compatibility_map
+
+ def assertBackwardCompatible(self, old_mojom, new_mojom):
+ compatibility_map = self._GetTypeCompatibilityMap(old_mojom, new_mojom)
+ for name, compatible in compatibility_map.items():
+ if not compatible:
+ raise AssertionError(
+ 'Given the old mojom:\n\n %s\n\nand the new mojom:\n\n %s\n\n'
+ 'The new definition of %s should pass a backward-compatibiity '
+ 'check, but it does not.' % (old_mojom, new_mojom, name))
+
+ def assertNotBackwardCompatible(self, old_mojom, new_mojom):
+ compatibility_map = self._GetTypeCompatibilityMap(old_mojom, new_mojom)
+ if all(compatibility_map.values()):
+ raise AssertionError(
+ 'Given the old mojom:\n\n %s\n\nand the new mojom:\n\n %s\n\n'
+ 'The new mojom should fail a backward-compatibility check, but it '
+ 'does not.' % (old_mojom, new_mojom))
+
+ def testNewNonExtensibleEnumValue(self):
+ """Adding a value to a non-extensible enum breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('enum E { kFoo, kBar };',
+ 'enum E { kFoo, kBar, kBaz };')
+
+ def testNewNonExtensibleEnumValueWithMinVersion(self):
+ """Adding a value to a non-extensible enum breaks backward-compatibility,
+ even with a new [MinVersion] specified for the value."""
+ self.assertNotBackwardCompatible(
+ 'enum E { kFoo, kBar };', 'enum E { kFoo, kBar, [MinVersion=1] kBaz };')
+
+ def testNewValueInExistingVersion(self):
+ """Adding a value to an existing version is not allowed, even if the old
+ enum was marked [Extensible]. Note that it is irrelevant whether or not the
+ new enum is marked [Extensible]."""
+ self.assertNotBackwardCompatible('[Extensible] enum E { kFoo, kBar };',
+ 'enum E { kFoo, kBar, kBaz };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kFoo, kBar };',
+ '[Extensible] enum E { kFoo, kBar, kBaz };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kFoo, [MinVersion=1] kBar };',
+ 'enum E { kFoo, [MinVersion=1] kBar, [MinVersion=1] kBaz };')
+
+ def testEnumValueRemoval(self):
+ """Removal of an enum value is never valid even for [Extensible] enums."""
+ self.assertNotBackwardCompatible('enum E { kFoo, kBar };',
+ 'enum E { kFoo };')
+ self.assertNotBackwardCompatible('[Extensible] enum E { kFoo, kBar };',
+ '[Extensible] enum E { kFoo };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kA, [MinVersion=1] kB };',
+ '[Extensible] enum E { kA, };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kA, [MinVersion=1] kB, [MinVersion=1] kZ };',
+ '[Extensible] enum E { kA, [MinVersion=1] kB };')
+
+ def testNewExtensibleEnumValueWithMinVersion(self):
+ """Adding a new and properly [MinVersion]'d value to an [Extensible] enum
+ is a backward-compatible change. Note that it is irrelevant whether or not
+ the new enum is marked [Extensible]."""
+ self.assertBackwardCompatible('[Extensible] enum E { kA, kB };',
+ 'enum E { kA, kB, [MinVersion=1] kC };')
+ self.assertBackwardCompatible(
+ '[Extensible] enum E { kA, kB };',
+ '[Extensible] enum E { kA, kB, [MinVersion=1] kC };')
+ self.assertBackwardCompatible(
+ '[Extensible] enum E { kA, [MinVersion=1] kB };',
+ '[Extensible] enum E { kA, [MinVersion=1] kB, [MinVersion=2] kC };')
+
+ def testRenameEnumValue(self):
+ """Renaming an enum value does not affect backward-compatibility. Only
+ numeric value is relevant."""
+ self.assertBackwardCompatible('enum E { kA, kB };', 'enum E { kX, kY };')
+
+ def testAddEnumValueAlias(self):
+ """Adding new enum fields does not affect backward-compatibility if it does
+ not introduce any new numeric values."""
+ self.assertBackwardCompatible(
+ 'enum E { kA, kB };', 'enum E { kA, kB, kC = kA, kD = 1, kE = kD };')
+
+ def testEnumIdentity(self):
+ """An unchanged enum is obviously backward-compatible."""
+ self.assertBackwardCompatible('enum E { kA, kB, kC };',
+ 'enum E { kA, kB, kC };')
+
+ def testNewStructFieldUnversioned(self):
+ """Adding a new field to a struct without a new (i.e. higher than any
+ existing version) [MinVersion] tag breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string a; };',
+ 'struct S { string a; string b; };')
+
+ def testStructFieldRemoval(self):
+ """Removing a field from a struct breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string a; string b; };',
+ 'struct S { string a; };')
+
+ def testStructFieldTypeChange(self):
+ """Changing the type of an existing field always breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string a; };',
+ 'struct S { array<int32> a; };')
+
+ def testStructFieldBecomingOptional(self):
+ """Changing a field from non-optional to optional breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string a; };',
+ 'struct S { string? a; };')
+
+ def testStructFieldBecomingNonOptional(self):
+ """Changing a field from optional to non-optional breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string? a; };',
+ 'struct S { string a; };')
+
+ def testStructFieldOrderChange(self):
+ """Changing the order of fields breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('struct S { string a; bool b; };',
+ 'struct S { bool b; string a; };')
+ self.assertNotBackwardCompatible('struct S { string a@0; bool b@1; };',
+ 'struct S { string a@1; bool b@0; };')
+
+ def testStructFieldMinVersionChange(self):
+ """Changing the MinVersion of a field breaks backward-compatibility."""
+ self.assertNotBackwardCompatible(
+ 'struct S { string a; [MinVersion=1] string? b; };',
+ 'struct S { string a; [MinVersion=2] string? b; };')
+
+ def testStructFieldTypeChange(self):
+ """If a struct field's own type definition changes, the containing struct
+ is backward-compatible if and only if the field type's change is
+ backward-compatible."""
+ self.assertBackwardCompatible(
+ 'struct S {}; struct T { S s; };',
+ 'struct S { [MinVersion=1] int32 x; }; struct T { S s; };')
+ self.assertBackwardCompatible(
+ '[Extensible] enum E { kA }; struct S { E e; };',
+ '[Extensible] enum E { kA, [MinVersion=1] kB }; struct S { E e; };')
+ self.assertNotBackwardCompatible(
+ 'struct S {}; struct T { S s; };',
+ 'struct S { int32 x; }; struct T { S s; };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kA }; struct S { E e; };',
+ '[Extensible] enum E { kA, kB }; struct S { E e; };')
+
+ def testNewStructFieldWithInvalidMinVersion(self):
+ """Adding a new field using an existing MinVersion breaks backward-
+ compatibility."""
+ self.assertNotBackwardCompatible(
+ """\
+ struct S {
+ string a;
+ [MinVersion=1] string? b;
+ };
+ """, """\
+ struct S {
+ string a;
+ [MinVersion=1] string? b;
+ [MinVersion=1] string? c;
+ };""")
+
+ def testNewStructFieldWithValidMinVersion(self):
+ """Adding a new field is safe if tagged with a MinVersion greater than any
+ previously used MinVersion in the struct."""
+ self.assertBackwardCompatible(
+ 'struct S { int32 a; };',
+ 'struct S { int32 a; [MinVersion=1] int32 b; };')
+ self.assertBackwardCompatible(
+ 'struct S { int32 a; [MinVersion=1] int32 b; };',
+ 'struct S { int32 a; [MinVersion=1] int32 b; [MinVersion=2] bool c; };')
+
+ def testNewStructFieldNullableReference(self):
+ """Adding a new nullable reference-typed field is fine if versioned
+ properly."""
+ self.assertBackwardCompatible(
+ 'struct S { int32 a; };',
+ 'struct S { int32 a; [MinVersion=1] string? b; };')
+
+ def testStructFieldRename(self):
+ """Renaming a field has no effect on backward-compatibility."""
+ self.assertBackwardCompatible('struct S { int32 x; bool b; };',
+ 'struct S { int32 a; bool b; };')
+
+ def testStructFieldReorderWithExplicitOrdinals(self):
+ """Reordering fields has no effect on backward-compatibility when field
+ ordinals are explicitly labeled and remain unchanged."""
+ self.assertBackwardCompatible('struct S { bool b@1; int32 a@0; };',
+ 'struct S { int32 a@0; bool b@1; };')
+
+ def testNewUnionFieldUnversioned(self):
+ """Adding a new field to a union without a new (i.e. higher than any
+ existing version) [MinVersion] tag breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string a; };',
+ 'union U { string a; string b; };')
+
+ def testUnionFieldRemoval(self):
+ """Removing a field from a union breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string a; string b; };',
+ 'union U { string a; };')
+
+ def testUnionFieldTypeChange(self):
+ """Changing the type of an existing field always breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string a; };',
+ 'union U { array<int32> a; };')
+
+ def testUnionFieldBecomingOptional(self):
+ """Changing a field from non-optional to optional breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string a; };',
+ 'union U { string? a; };')
+
+ def testUnionFieldBecomingNonOptional(self):
+ """Changing a field from optional to non-optional breaks
+ backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string? a; };',
+ 'union U { string a; };')
+
+ def testUnionFieldOrderChange(self):
+ """Changing the order of fields breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('union U { string a; bool b; };',
+ 'union U { bool b; string a; };')
+ self.assertNotBackwardCompatible('union U { string a@0; bool b@1; };',
+ 'union U { string a@1; bool b@0; };')
+
+ def testUnionFieldMinVersionChange(self):
+ """Changing the MinVersion of a field breaks backward-compatibility."""
+ self.assertNotBackwardCompatible(
+ 'union U { string a; [MinVersion=1] string b; };',
+ 'union U { string a; [MinVersion=2] string b; };')
+
+ def testUnionFieldTypeChange(self):
+ """If a union field's own type definition changes, the containing union
+ is backward-compatible if and only if the field type's change is
+ backward-compatible."""
+ self.assertBackwardCompatible(
+ 'struct S {}; union U { S s; };',
+ 'struct S { [MinVersion=1] int32 x; }; union U { S s; };')
+ self.assertBackwardCompatible(
+ '[Extensible] enum E { kA }; union U { E e; };',
+ '[Extensible] enum E { kA, [MinVersion=1] kB }; union U { E e; };')
+ self.assertNotBackwardCompatible(
+ 'struct S {}; union U { S s; };',
+ 'struct S { int32 x; }; union U { S s; };')
+ self.assertNotBackwardCompatible(
+ '[Extensible] enum E { kA }; union U { E e; };',
+ '[Extensible] enum E { kA, kB }; union U { E e; };')
+
+ def testNewUnionFieldWithInvalidMinVersion(self):
+ """Adding a new field using an existing MinVersion breaks backward-
+ compatibility."""
+ self.assertNotBackwardCompatible(
+ """\
+ union U {
+ string a;
+ [MinVersion=1] string b;
+ };
+ """, """\
+ union U {
+ string a;
+ [MinVersion=1] string b;
+ [MinVersion=1] string c;
+ };""")
+
+ def testNewUnionFieldWithValidMinVersion(self):
+ """Adding a new field is safe if tagged with a MinVersion greater than any
+ previously used MinVersion in the union."""
+ self.assertBackwardCompatible(
+ 'union U { int32 a; };',
+ 'union U { int32 a; [MinVersion=1] int32 b; };')
+ self.assertBackwardCompatible(
+ 'union U { int32 a; [MinVersion=1] int32 b; };',
+ 'union U { int32 a; [MinVersion=1] int32 b; [MinVersion=2] bool c; };')
+
+ def testUnionFieldRename(self):
+ """Renaming a field has no effect on backward-compatibility."""
+ self.assertBackwardCompatible('union U { int32 x; bool b; };',
+ 'union U { int32 a; bool b; };')
+
+ def testUnionFieldReorderWithExplicitOrdinals(self):
+ """Reordering fields has no effect on backward-compatibility when field
+ ordinals are explicitly labeled and remain unchanged."""
+ self.assertBackwardCompatible('union U { bool b@1; int32 a@0; };',
+ 'union U { int32 a@0; bool b@1; };')
+
+ def testNewInterfaceMethodUnversioned(self):
+ """Adding a new method to an interface without a new (i.e. higher than any
+ existing version) [MinVersion] tag breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('interface F { A(); };',
+ 'interface F { A(); B(); };')
+
+ def testInterfaceMethodRemoval(self):
+ """Removing a method from an interface breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('interface F { A(); B(); };',
+ 'interface F { A(); };')
+
+ def testInterfaceMethodParamsChanged(self):
+ """Changes to the parameter list are only backward-compatible if they meet
+ backward-compatibility requirements of an equivalent struct definition."""
+ self.assertNotBackwardCompatible('interface F { A(); };',
+ 'interface F { A(int32 x); };')
+ self.assertNotBackwardCompatible('interface F { A(int32 x); };',
+ 'interface F { A(bool x); };')
+ self.assertNotBackwardCompatible(
+ 'interface F { A(int32 x, [MinVersion=1] string? s); };', """\
+ interface F {
+ A(int32 x, [MinVersion=1] string? s, [MinVersion=1] int32 y);
+ };""")
+
+ self.assertBackwardCompatible('interface F { A(int32 x); };',
+ 'interface F { A(int32 a); };')
+ self.assertBackwardCompatible(
+ 'interface F { A(int32 x); };',
+ 'interface F { A(int32 x, [MinVersion=1] string? s); };')
+
+ self.assertBackwardCompatible(
+ 'struct S {}; interface F { A(S s); };',
+ 'struct S { [MinVersion=1] int32 x; }; interface F { A(S s); };')
+ self.assertBackwardCompatible(
+ 'struct S {}; struct T {}; interface F { A(S s); };',
+ 'struct S {}; struct T {}; interface F { A(T s); };')
+ self.assertNotBackwardCompatible(
+ 'struct S {}; struct T { int32 x; }; interface F { A(S s); };',
+ 'struct S {}; struct T { int32 x; }; interface F { A(T t); };')
+
+ def testInterfaceMethodReplyAdded(self):
+ """Adding a reply to a message breaks backward-compatibilty."""
+ self.assertNotBackwardCompatible('interface F { A(); };',
+ 'interface F { A() => (); };')
+
+ def testInterfaceMethodReplyRemoved(self):
+ """Removing a reply from a message breaks backward-compatibility."""
+ self.assertNotBackwardCompatible('interface F { A() => (); };',
+ 'interface F { A(); };')
+
+ def testInterfaceMethodReplyParamsChanged(self):
+ """Similar to request parameters, a change to reply parameters is considered
+ backward-compatible if it meets the same backward-compatibility
+ requirements imposed on equivalent struct changes."""
+ self.assertNotBackwardCompatible('interface F { A() => (); };',
+ 'interface F { A() => (int32 x); };')
+ self.assertNotBackwardCompatible('interface F { A() => (int32 x); };',
+ 'interface F { A() => (); };')
+ self.assertNotBackwardCompatible('interface F { A() => (bool x); };',
+ 'interface F { A() => (int32 x); };')
+
+ self.assertBackwardCompatible('interface F { A() => (int32 a); };',
+ 'interface F { A() => (int32 x); };')
+ self.assertBackwardCompatible(
+ 'interface F { A() => (int32 x); };',
+ 'interface F { A() => (int32 x, [MinVersion] string? s); };')
+
+ def testNewInterfaceMethodWithInvalidMinVersion(self):
+ """Adding a new method to an existing version is not backward-compatible."""
+ self.assertNotBackwardCompatible(
+ """\
+ interface F {
+ A();
+ [MinVersion=1] B();
+ };
+ """, """\
+ interface F {
+ A();
+ [MinVersion=1] B();
+ [MinVersion=1] C();
+ };
+ """)
+
+ def testNewInterfaceMethodWithValidMinVersion(self):
+ """Adding a new method is fine as long as its MinVersion exceeds that of any
+ method on the old interface definition."""
+ self.assertBackwardCompatible('interface F { A(); };',
+ 'interface F { A(); [MinVersion=1] B(); };')