blob: 0a489ee11c1cb4fdae95d7d0dd2a72c1659b9177 [file] [log] [blame]
Paul Duffin428c6512021-07-21 15:33:22 +01001#!/usr/bin/env python
2#
3# Copyright (C) 2021 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the 'License');
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an 'AS IS' BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
Paul Duffin428c6512021-07-21 15:33:22 +010016"""Unit tests for verify_overlaps_test.py."""
17import io
18import unittest
19
Paul Duffin181b56c2022-04-08 00:03:32 +010020import verify_overlaps as vo
Spandan Das559132f2021-08-25 18:17:33 +000021
Paul Duffin428c6512021-07-21 15:33:22 +010022
23class TestDetectOverlaps(unittest.TestCase):
24
Paul Duffin181b56c2022-04-08 00:03:32 +010025 @staticmethod
26 def read_flag_trie_from_string(csvdata):
Spandan Das559132f2021-08-25 18:17:33 +000027 with io.StringIO(csvdata) as f:
Paul Duffin181b56c2022-04-08 00:03:32 +010028 return vo.read_flag_trie_from_stream(f)
Paul Duffinc11f6672021-07-20 00:04:21 +010029
Paul Duffin181b56c2022-04-08 00:03:32 +010030 @staticmethod
31 def read_signature_csv_from_string_as_dict(csvdata):
Spandan Das559132f2021-08-25 18:17:33 +000032 with io.StringIO(csvdata) as f:
Paul Duffin181b56c2022-04-08 00:03:32 +010033 return vo.read_signature_csv_from_stream_as_dict(f)
Paul Duffin428c6512021-07-21 15:33:22 +010034
Paul Duffin181b56c2022-04-08 00:03:32 +010035 @staticmethod
Spandan Das559132f2021-08-25 18:17:33 +000036 def extract_subset_from_monolithic_flags_as_dict_from_string(
Paul Duffin181b56c2022-04-08 00:03:32 +010037 monolithic, patterns):
Paul Duffin67b9d612021-07-21 17:38:47 +010038 with io.StringIO(patterns) as f:
Paul Duffin181b56c2022-04-08 00:03:32 +010039 return vo.extract_subset_from_monolithic_flags_as_dict_from_stream(
Spandan Das559132f2021-08-25 18:17:33 +000040 monolithic, f)
Paul Duffin67b9d612021-07-21 17:38:47 +010041
Spandan Das559132f2021-08-25 18:17:33 +000042 extractInput = """
Paul Duffin53a76072021-07-21 17:27:09 +010043Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
44Ljava/lang/Object;->toString()Ljava/lang/String;,blocked
Paul Duffinc11f6672021-07-20 00:04:21 +010045Ljava/util/zip/ZipFile;-><clinit>()V,blocked
46Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;,blocked
47Ljava/lang/Character;->serialVersionUID:J,sdk
48Ljava/lang/ProcessBuilder$Redirect$1;-><init>()V,blocked
Spandan Das559132f2021-08-25 18:17:33 +000049"""
Paul Duffin53a76072021-07-21 17:27:09 +010050
Paul Duffinc11f6672021-07-20 00:04:21 +010051 def test_extract_subset_signature(self):
Spandan Das559132f2021-08-25 18:17:33 +000052 monolithic = self.read_flag_trie_from_string(
53 TestDetectOverlaps.extractInput)
Paul Duffin67b9d612021-07-21 17:38:47 +010054
Paul Duffin181b56c2022-04-08 00:03:32 +010055 patterns = "Ljava/lang/Object;->hashCode()I"
Paul Duffin67b9d612021-07-21 17:38:47 +010056
Spandan Das559132f2021-08-25 18:17:33 +000057 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
58 monolithic, patterns)
Paul Duffin53a76072021-07-21 17:27:09 +010059 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +010060 "Ljava/lang/Object;->hashCode()I": {
61 None: ["public-api", "system-api", "test-api"],
62 "signature": "Ljava/lang/Object;->hashCode()I",
Paul Duffin53a76072021-07-21 17:27:09 +010063 },
64 }
65 self.assertEqual(expected, subset)
66
Paul Duffinc11f6672021-07-20 00:04:21 +010067 def test_extract_subset_class(self):
Spandan Das559132f2021-08-25 18:17:33 +000068 monolithic = self.read_flag_trie_from_string(
69 TestDetectOverlaps.extractInput)
Paul Duffinc11f6672021-07-20 00:04:21 +010070
Paul Duffin181b56c2022-04-08 00:03:32 +010071 patterns = "java/lang/Object"
Paul Duffinc11f6672021-07-20 00:04:21 +010072
Spandan Das559132f2021-08-25 18:17:33 +000073 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
74 monolithic, patterns)
Paul Duffinc11f6672021-07-20 00:04:21 +010075 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +010076 "Ljava/lang/Object;->hashCode()I": {
77 None: ["public-api", "system-api", "test-api"],
78 "signature": "Ljava/lang/Object;->hashCode()I",
Paul Duffinc11f6672021-07-20 00:04:21 +010079 },
Paul Duffin181b56c2022-04-08 00:03:32 +010080 "Ljava/lang/Object;->toString()Ljava/lang/String;": {
81 None: ["blocked"],
82 "signature": "Ljava/lang/Object;->toString()Ljava/lang/String;",
Paul Duffinc11f6672021-07-20 00:04:21 +010083 },
84 }
85 self.assertEqual(expected, subset)
86
87 def test_extract_subset_outer_class(self):
Spandan Das559132f2021-08-25 18:17:33 +000088 monolithic = self.read_flag_trie_from_string(
89 TestDetectOverlaps.extractInput)
Paul Duffinc11f6672021-07-20 00:04:21 +010090
Paul Duffin181b56c2022-04-08 00:03:32 +010091 patterns = "java/lang/Character"
Paul Duffinc11f6672021-07-20 00:04:21 +010092
Spandan Das559132f2021-08-25 18:17:33 +000093 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
94 monolithic, patterns)
Paul Duffinc11f6672021-07-20 00:04:21 +010095 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +010096 "Ljava/lang/Character$UnicodeScript;"
97 "->of(I)Ljava/lang/Character$UnicodeScript;": {
98 None: ["blocked"],
99 "signature": "Ljava/lang/Character$UnicodeScript;"
100 "->of(I)Ljava/lang/Character$UnicodeScript;",
101 },
102 "Ljava/lang/Character;->serialVersionUID:J": {
103 None: ["sdk"],
104 "signature": "Ljava/lang/Character;->serialVersionUID:J",
Paul Duffinc11f6672021-07-20 00:04:21 +0100105 },
106 }
107 self.assertEqual(expected, subset)
108
109 def test_extract_subset_nested_class(self):
Spandan Das559132f2021-08-25 18:17:33 +0000110 monolithic = self.read_flag_trie_from_string(
111 TestDetectOverlaps.extractInput)
Paul Duffinc11f6672021-07-20 00:04:21 +0100112
Paul Duffin181b56c2022-04-08 00:03:32 +0100113 patterns = "java/lang/Character$UnicodeScript"
Paul Duffinc11f6672021-07-20 00:04:21 +0100114
Spandan Das559132f2021-08-25 18:17:33 +0000115 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
116 monolithic, patterns)
Paul Duffinc11f6672021-07-20 00:04:21 +0100117 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +0100118 "Ljava/lang/Character$UnicodeScript;"
119 "->of(I)Ljava/lang/Character$UnicodeScript;": {
120 None: ["blocked"],
121 "signature": "Ljava/lang/Character$UnicodeScript;"
122 "->of(I)Ljava/lang/Character$UnicodeScript;",
123 },
Paul Duffinc11f6672021-07-20 00:04:21 +0100124 }
125 self.assertEqual(expected, subset)
126
127 def test_extract_subset_package(self):
Spandan Das559132f2021-08-25 18:17:33 +0000128 monolithic = self.read_flag_trie_from_string(
129 TestDetectOverlaps.extractInput)
Paul Duffinc11f6672021-07-20 00:04:21 +0100130
Paul Duffin181b56c2022-04-08 00:03:32 +0100131 patterns = "java/lang/*"
Paul Duffinc11f6672021-07-20 00:04:21 +0100132
Spandan Das559132f2021-08-25 18:17:33 +0000133 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
134 monolithic, patterns)
Paul Duffinc11f6672021-07-20 00:04:21 +0100135 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +0100136 "Ljava/lang/Character$UnicodeScript;"
137 "->of(I)Ljava/lang/Character$UnicodeScript;": {
138 None: ["blocked"],
139 "signature": "Ljava/lang/Character$UnicodeScript;"
140 "->of(I)Ljava/lang/Character$UnicodeScript;",
Paul Duffinc11f6672021-07-20 00:04:21 +0100141 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100142 "Ljava/lang/Character;->serialVersionUID:J": {
143 None: ["sdk"],
144 "signature": "Ljava/lang/Character;->serialVersionUID:J",
Paul Duffinc11f6672021-07-20 00:04:21 +0100145 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100146 "Ljava/lang/Object;->hashCode()I": {
147 None: ["public-api", "system-api", "test-api"],
148 "signature": "Ljava/lang/Object;->hashCode()I",
Paul Duffinc11f6672021-07-20 00:04:21 +0100149 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100150 "Ljava/lang/Object;->toString()Ljava/lang/String;": {
151 None: ["blocked"],
152 "signature": "Ljava/lang/Object;->toString()Ljava/lang/String;",
153 },
154 "Ljava/lang/ProcessBuilder$Redirect$1;-><init>()V": {
155 None: ["blocked"],
156 "signature": "Ljava/lang/ProcessBuilder$Redirect$1;-><init>()V",
Paul Duffinc11f6672021-07-20 00:04:21 +0100157 },
158 }
159 self.assertEqual(expected, subset)
160
161 def test_extract_subset_recursive_package(self):
Spandan Das559132f2021-08-25 18:17:33 +0000162 monolithic = self.read_flag_trie_from_string(
163 TestDetectOverlaps.extractInput)
Paul Duffinc11f6672021-07-20 00:04:21 +0100164
Paul Duffin181b56c2022-04-08 00:03:32 +0100165 patterns = "java/**"
Paul Duffinc11f6672021-07-20 00:04:21 +0100166
Spandan Das559132f2021-08-25 18:17:33 +0000167 subset = self.extract_subset_from_monolithic_flags_as_dict_from_string(
168 monolithic, patterns)
Paul Duffinc11f6672021-07-20 00:04:21 +0100169 expected = {
Paul Duffin181b56c2022-04-08 00:03:32 +0100170 "Ljava/lang/Character$UnicodeScript;"
171 "->of(I)Ljava/lang/Character$UnicodeScript;": {
172 None: ["blocked"],
173 "signature": "Ljava/lang/Character$UnicodeScript;"
174 "->of(I)Ljava/lang/Character$UnicodeScript;",
Paul Duffinc11f6672021-07-20 00:04:21 +0100175 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100176 "Ljava/lang/Character;->serialVersionUID:J": {
177 None: ["sdk"],
178 "signature": "Ljava/lang/Character;->serialVersionUID:J",
Paul Duffinc11f6672021-07-20 00:04:21 +0100179 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100180 "Ljava/lang/Object;->hashCode()I": {
181 None: ["public-api", "system-api", "test-api"],
182 "signature": "Ljava/lang/Object;->hashCode()I",
Paul Duffinc11f6672021-07-20 00:04:21 +0100183 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100184 "Ljava/lang/Object;->toString()Ljava/lang/String;": {
185 None: ["blocked"],
186 "signature": "Ljava/lang/Object;->toString()Ljava/lang/String;",
Paul Duffinc11f6672021-07-20 00:04:21 +0100187 },
Paul Duffin181b56c2022-04-08 00:03:32 +0100188 "Ljava/lang/ProcessBuilder$Redirect$1;-><init>()V": {
189 None: ["blocked"],
190 "signature": "Ljava/lang/ProcessBuilder$Redirect$1;-><init>()V",
191 },
192 "Ljava/util/zip/ZipFile;-><clinit>()V": {
193 None: ["blocked"],
194 "signature": "Ljava/util/zip/ZipFile;-><clinit>()V",
Paul Duffinc11f6672021-07-20 00:04:21 +0100195 },
196 }
197 self.assertEqual(expected, subset)
198
Paul Duffinc11f6672021-07-20 00:04:21 +0100199 def test_read_trie_duplicate(self):
200 with self.assertRaises(Exception) as context:
Spandan Das559132f2021-08-25 18:17:33 +0000201 self.read_flag_trie_from_string("""
Paul Duffinc11f6672021-07-20 00:04:21 +0100202Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
203Ljava/lang/Object;->hashCode()I,blocked
Spandan Das559132f2021-08-25 18:17:33 +0000204""")
Paul Duffin181b56c2022-04-08 00:03:32 +0100205 self.assertTrue("Duplicate signature: Ljava/lang/Object;->hashCode()I"
Spandan Das559132f2021-08-25 18:17:33 +0000206 in str(context.exception))
Paul Duffinc11f6672021-07-20 00:04:21 +0100207
208 def test_read_trie_missing_member(self):
209 with self.assertRaises(Exception) as context:
Spandan Das559132f2021-08-25 18:17:33 +0000210 self.read_flag_trie_from_string("""
Paul Duffinc11f6672021-07-20 00:04:21 +0100211Ljava/lang/Object,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000212""")
213 self.assertTrue(
Paul Duffin181b56c2022-04-08 00:03:32 +0100214 "Invalid signature: Ljava/lang/Object, "
215 "does not identify a specific member" in str(context.exception))
Paul Duffinc11f6672021-07-20 00:04:21 +0100216
Paul Duffin428c6512021-07-21 15:33:22 +0100217 def test_match(self):
Spandan Das559132f2021-08-25 18:17:33 +0000218 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100219Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000220""")
221 modular = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100222Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000223""")
Paul Duffinbd88c882022-04-07 23:32:19 +0100224 mismatches = vo.compare_signature_flags(monolithic, modular,
225 ["blocked"])
Paul Duffin428c6512021-07-21 15:33:22 +0100226 expected = []
227 self.assertEqual(expected, mismatches)
228
229 def test_mismatch_overlapping_flags(self):
Spandan Das559132f2021-08-25 18:17:33 +0000230 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100231Ljava/lang/Object;->toString()Ljava/lang/String;,public-api
Spandan Das559132f2021-08-25 18:17:33 +0000232""")
233 modular = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100234Ljava/lang/Object;->toString()Ljava/lang/String;,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000235""")
Paul Duffinbd88c882022-04-07 23:32:19 +0100236 mismatches = vo.compare_signature_flags(monolithic, modular,
237 ["blocked"])
Paul Duffin428c6512021-07-21 15:33:22 +0100238 expected = [
239 (
Paul Duffin181b56c2022-04-08 00:03:32 +0100240 "Ljava/lang/Object;->toString()Ljava/lang/String;",
241 ["public-api", "system-api", "test-api"],
242 ["public-api"],
Paul Duffin428c6512021-07-21 15:33:22 +0100243 ),
244 ]
245 self.assertEqual(expected, mismatches)
246
Paul Duffin428c6512021-07-21 15:33:22 +0100247 def test_mismatch_monolithic_blocked(self):
Spandan Das559132f2021-08-25 18:17:33 +0000248 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100249Ljava/lang/Object;->toString()Ljava/lang/String;,blocked
Spandan Das559132f2021-08-25 18:17:33 +0000250""")
251 modular = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100252Ljava/lang/Object;->toString()Ljava/lang/String;,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000253""")
Paul Duffinbd88c882022-04-07 23:32:19 +0100254 mismatches = vo.compare_signature_flags(monolithic, modular,
255 ["blocked"])
Paul Duffin428c6512021-07-21 15:33:22 +0100256 expected = [
257 (
Paul Duffin181b56c2022-04-08 00:03:32 +0100258 "Ljava/lang/Object;->toString()Ljava/lang/String;",
259 ["public-api", "system-api", "test-api"],
260 ["blocked"],
Paul Duffin428c6512021-07-21 15:33:22 +0100261 ),
262 ]
263 self.assertEqual(expected, mismatches)
264
265 def test_mismatch_modular_blocked(self):
Spandan Das559132f2021-08-25 18:17:33 +0000266 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100267Ljava/lang/Object;->toString()Ljava/lang/String;,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000268""")
269 modular = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100270Ljava/lang/Object;->toString()Ljava/lang/String;,blocked
Spandan Das559132f2021-08-25 18:17:33 +0000271""")
Paul Duffinbd88c882022-04-07 23:32:19 +0100272 mismatches = vo.compare_signature_flags(monolithic, modular,
273 ["blocked"])
Paul Duffin428c6512021-07-21 15:33:22 +0100274 expected = [
275 (
Paul Duffin181b56c2022-04-08 00:03:32 +0100276 "Ljava/lang/Object;->toString()Ljava/lang/String;",
277 ["blocked"],
278 ["public-api", "system-api", "test-api"],
Paul Duffin428c6512021-07-21 15:33:22 +0100279 ),
280 ]
281 self.assertEqual(expected, mismatches)
282
Paul Duffin280bae62021-07-20 18:03:53 +0100283 def test_match_treat_missing_from_modular_as_blocked(self):
Paul Duffin181b56c2022-04-08 00:03:32 +0100284 monolithic = self.read_signature_csv_from_string_as_dict("")
Spandan Das559132f2021-08-25 18:17:33 +0000285 modular = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100286Ljava/lang/Object;->toString()Ljava/lang/String;,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000287""")
Paul Duffinbd88c882022-04-07 23:32:19 +0100288 mismatches = vo.compare_signature_flags(monolithic, modular,
289 ["blocked"])
Paul Duffin428c6512021-07-21 15:33:22 +0100290 expected = [
291 (
Paul Duffin181b56c2022-04-08 00:03:32 +0100292 "Ljava/lang/Object;->toString()Ljava/lang/String;",
293 ["public-api", "system-api", "test-api"],
Paul Duffin428c6512021-07-21 15:33:22 +0100294 [],
295 ),
296 ]
297 self.assertEqual(expected, mismatches)
298
Paul Duffin280bae62021-07-20 18:03:53 +0100299 def test_mismatch_treat_missing_from_modular_as_blocked(self):
Spandan Das559132f2021-08-25 18:17:33 +0000300 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100301Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
Spandan Das559132f2021-08-25 18:17:33 +0000302""")
Paul Duffin428c6512021-07-21 15:33:22 +0100303 modular = {}
Paul Duffinbd88c882022-04-07 23:32:19 +0100304 mismatches = vo.compare_signature_flags(monolithic, modular,
305 ["blocked"])
Paul Duffin53a76072021-07-21 17:27:09 +0100306 expected = [
307 (
Paul Duffin181b56c2022-04-08 00:03:32 +0100308 "Ljava/lang/Object;->hashCode()I",
309 ["blocked"],
310 ["public-api", "system-api", "test-api"],
Paul Duffin53a76072021-07-21 17:27:09 +0100311 ),
312 ]
Paul Duffin428c6512021-07-21 15:33:22 +0100313 self.assertEqual(expected, mismatches)
314
315 def test_blocked_missing_from_modular(self):
Spandan Das559132f2021-08-25 18:17:33 +0000316 monolithic = self.read_signature_csv_from_string_as_dict("""
Paul Duffin428c6512021-07-21 15:33:22 +0100317Ljava/lang/Object;->hashCode()I,blocked
Spandan Das559132f2021-08-25 18:17:33 +0000318""")
Paul Duffin428c6512021-07-21 15:33:22 +0100319 modular = {}
Paul Duffinbd88c882022-04-07 23:32:19 +0100320 mismatches = vo.compare_signature_flags(monolithic, modular,
321 ["blocked"])
322 expected = []
323 self.assertEqual(expected, mismatches)
324
325 def test_match_treat_missing_from_modular_as_empty(self):
326 monolithic = self.read_signature_csv_from_string_as_dict("")
327 modular = self.read_signature_csv_from_string_as_dict("""
328Ljava/lang/Object;->toString()Ljava/lang/String;,public-api,system-api,test-api
329""")
330 mismatches = vo.compare_signature_flags(monolithic, modular, [])
331 expected = [
332 (
333 "Ljava/lang/Object;->toString()Ljava/lang/String;",
334 ["public-api", "system-api", "test-api"],
335 [],
336 ),
337 ]
338 self.assertEqual(expected, mismatches)
339
340 def test_mismatch_treat_missing_from_modular_as_empty(self):
341 monolithic = self.read_signature_csv_from_string_as_dict("""
342Ljava/lang/Object;->hashCode()I,public-api,system-api,test-api
343""")
344 modular = {}
345 mismatches = vo.compare_signature_flags(monolithic, modular, [])
346 expected = [
347 (
348 "Ljava/lang/Object;->hashCode()I",
349 [],
350 ["public-api", "system-api", "test-api"],
351 ),
352 ]
353 self.assertEqual(expected, mismatches)
354
355 def test_empty_missing_from_modular(self):
356 monolithic = self.read_signature_csv_from_string_as_dict("""
357Ljava/lang/Object;->hashCode()I
358""")
359 modular = {}
360 mismatches = vo.compare_signature_flags(monolithic, modular, [])
Paul Duffin280bae62021-07-20 18:03:53 +0100361 expected = []
Paul Duffin428c6512021-07-21 15:33:22 +0100362 self.assertEqual(expected, mismatches)
Paul Duffinb5cd5222022-02-28 19:06:49 +0000363
364
Paul Duffin181b56c2022-04-08 00:03:32 +0100365if __name__ == "__main__":
Paul Duffin428c6512021-07-21 15:33:22 +0100366 unittest.main(verbosity=2)