1a8e1175bSopenharmony_ci#!/usr/bin/env python3 2a8e1175bSopenharmony_ci"""This script compares the interfaces of two versions of Mbed TLS, looking 3a8e1175bSopenharmony_cifor backward incompatibilities between two different Git revisions within 4a8e1175bSopenharmony_cian Mbed TLS repository. It must be run from the root of a Git working tree. 5a8e1175bSopenharmony_ci 6a8e1175bSopenharmony_ci### How the script works ### 7a8e1175bSopenharmony_ci 8a8e1175bSopenharmony_ciFor the source (API) and runtime (ABI) interface compatibility, this script 9a8e1175bSopenharmony_ciis a small wrapper around the abi-compliance-checker and abi-dumper tools, 10a8e1175bSopenharmony_ciapplying them to compare the header and library files. 11a8e1175bSopenharmony_ci 12a8e1175bSopenharmony_ciFor the storage format, this script compares the automatically generated 13a8e1175bSopenharmony_cistorage tests and the manual read tests, and complains if there is a 14a8e1175bSopenharmony_cireduction in coverage. A change in test data will be signaled as a 15a8e1175bSopenharmony_cicoverage reduction since the old test data is no longer present. A change in 16a8e1175bSopenharmony_cihow test data is presented will be signaled as well; this would be a false 17a8e1175bSopenharmony_cipositive. 18a8e1175bSopenharmony_ci 19a8e1175bSopenharmony_ciThe results of the API/ABI comparison are either formatted as HTML and stored 20a8e1175bSopenharmony_ciat a configurable location, or are given as a brief list of problems. 21a8e1175bSopenharmony_ciReturns 0 on success, 1 on non-compliance, and 2 if there is an error 22a8e1175bSopenharmony_ciwhile running the script. 23a8e1175bSopenharmony_ci 24a8e1175bSopenharmony_ci### How to interpret non-compliance ### 25a8e1175bSopenharmony_ci 26a8e1175bSopenharmony_ciThis script has relatively common false positives. In many scenarios, it only 27a8e1175bSopenharmony_cireports a pass if there is a strict textual match between the old version and 28a8e1175bSopenharmony_cithe new version, and it reports problems where there is a sufficient semantic 29a8e1175bSopenharmony_cimatch but not a textual match. This section lists some common false positives. 30a8e1175bSopenharmony_ciThis is not an exhaustive list: in the end what matters is whether we are 31a8e1175bSopenharmony_cibreaking a backward compatibility goal. 32a8e1175bSopenharmony_ci 33a8e1175bSopenharmony_ci**API**: the goal is that if an application works with the old version of the 34a8e1175bSopenharmony_cilibrary, it can be recompiled against the new version and will still work. 35a8e1175bSopenharmony_ciThis is normally validated by comparing the declarations in `include/*/*.h`. 36a8e1175bSopenharmony_ciA failure is a declaration that has disappeared or that now has a different 37a8e1175bSopenharmony_citype. 38a8e1175bSopenharmony_ci 39a8e1175bSopenharmony_ci * It's ok to change or remove macros and functions that are documented as 40a8e1175bSopenharmony_ci for internal use only or as experimental. 41a8e1175bSopenharmony_ci * It's ok to rename function or macro parameters as long as the semantics 42a8e1175bSopenharmony_ci has not changed. 43a8e1175bSopenharmony_ci * It's ok to change or remove structure fields that are documented as 44a8e1175bSopenharmony_ci private. 45a8e1175bSopenharmony_ci * It's ok to add fields to a structure that already had private fields 46a8e1175bSopenharmony_ci or was documented as extensible. 47a8e1175bSopenharmony_ci 48a8e1175bSopenharmony_ci**ABI**: the goal is that if an application was built against the old version 49a8e1175bSopenharmony_ciof the library, the same binary will work when linked against the new version. 50a8e1175bSopenharmony_ciThis is normally validated by comparing the symbols exported by `libmbed*.so`. 51a8e1175bSopenharmony_ciA failure is a symbol that is no longer exported by the same library or that 52a8e1175bSopenharmony_cinow has a different type. 53a8e1175bSopenharmony_ci 54a8e1175bSopenharmony_ci * All ABI changes are acceptable if the library version is bumped 55a8e1175bSopenharmony_ci (see `scripts/bump_version.sh`). 56a8e1175bSopenharmony_ci * ABI changes that concern functions which are declared only inside the 57a8e1175bSopenharmony_ci library directory, and not in `include/*/*.h`, are acceptable only if 58a8e1175bSopenharmony_ci the function was only ever used inside the same library (libmbedcrypto, 59a8e1175bSopenharmony_ci libmbedx509, libmbedtls). As a counter example, if the old version 60a8e1175bSopenharmony_ci of libmbedtls calls mbedtls_foo() from libmbedcrypto, and the new version 61a8e1175bSopenharmony_ci of libmbedcrypto no longer has a compatible mbedtls_foo(), this does 62a8e1175bSopenharmony_ci require a version bump for libmbedcrypto. 63a8e1175bSopenharmony_ci 64a8e1175bSopenharmony_ci**Storage format**: the goal is to check that persistent keys stored by the 65a8e1175bSopenharmony_ciold version can be read by the new version. This is normally validated by 66a8e1175bSopenharmony_cicomparing the `*read*` test cases in `test_suite*storage_format*.data`. 67a8e1175bSopenharmony_ciA failure is a storage read test case that is no longer present with the same 68a8e1175bSopenharmony_cifunction name and parameter list. 69a8e1175bSopenharmony_ci 70a8e1175bSopenharmony_ci * It's ok if the same test data is present, but its presentation has changed, 71a8e1175bSopenharmony_ci for example if a test function is renamed or has different parameters. 72a8e1175bSopenharmony_ci * It's ok if redundant tests are removed. 73a8e1175bSopenharmony_ci 74a8e1175bSopenharmony_ci**Generated test coverage**: the goal is to check that automatically 75a8e1175bSopenharmony_cigenerated tests have as much coverage as before. This is normally validated 76a8e1175bSopenharmony_ciby comparing the test cases that are automatically generated by a script. 77a8e1175bSopenharmony_ciA failure is a generated test case that is no longer present with the same 78a8e1175bSopenharmony_cifunction name and parameter list. 79a8e1175bSopenharmony_ci 80a8e1175bSopenharmony_ci * It's ok if the same test data is present, but its presentation has changed, 81a8e1175bSopenharmony_ci for example if a test function is renamed or has different parameters. 82a8e1175bSopenharmony_ci * It's ok if redundant tests are removed. 83a8e1175bSopenharmony_ci 84a8e1175bSopenharmony_ci""" 85a8e1175bSopenharmony_ci 86a8e1175bSopenharmony_ci# Copyright The Mbed TLS Contributors 87a8e1175bSopenharmony_ci# SPDX-License-Identifier: Apache-2.0 88a8e1175bSopenharmony_ci# 89a8e1175bSopenharmony_ci# Licensed under the Apache License, Version 2.0 (the "License"); you may 90a8e1175bSopenharmony_ci# not use this file except in compliance with the License. 91a8e1175bSopenharmony_ci# You may obtain a copy of the License at 92a8e1175bSopenharmony_ci# 93a8e1175bSopenharmony_ci# http://www.apache.org/licenses/LICENSE-2.0 94a8e1175bSopenharmony_ci# 95a8e1175bSopenharmony_ci# Unless required by applicable law or agreed to in writing, software 96a8e1175bSopenharmony_ci# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 97a8e1175bSopenharmony_ci# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 98a8e1175bSopenharmony_ci# See the License for the specific language governing permissions and 99a8e1175bSopenharmony_ci# limitations under the License. 100a8e1175bSopenharmony_ci 101a8e1175bSopenharmony_ciimport glob 102a8e1175bSopenharmony_ciimport os 103a8e1175bSopenharmony_ciimport re 104a8e1175bSopenharmony_ciimport sys 105a8e1175bSopenharmony_ciimport traceback 106a8e1175bSopenharmony_ciimport shutil 107a8e1175bSopenharmony_ciimport subprocess 108a8e1175bSopenharmony_ciimport argparse 109a8e1175bSopenharmony_ciimport logging 110a8e1175bSopenharmony_ciimport tempfile 111a8e1175bSopenharmony_ciimport fnmatch 112a8e1175bSopenharmony_cifrom types import SimpleNamespace 113a8e1175bSopenharmony_ci 114a8e1175bSopenharmony_ciimport xml.etree.ElementTree as ET 115a8e1175bSopenharmony_ci 116a8e1175bSopenharmony_cifrom mbedtls_dev import build_tree 117a8e1175bSopenharmony_ci 118a8e1175bSopenharmony_ci 119a8e1175bSopenharmony_ciclass AbiChecker: 120a8e1175bSopenharmony_ci """API and ABI checker.""" 121a8e1175bSopenharmony_ci 122a8e1175bSopenharmony_ci def __init__(self, old_version, new_version, configuration): 123a8e1175bSopenharmony_ci """Instantiate the API/ABI checker. 124a8e1175bSopenharmony_ci 125a8e1175bSopenharmony_ci old_version: RepoVersion containing details to compare against 126a8e1175bSopenharmony_ci new_version: RepoVersion containing details to check 127a8e1175bSopenharmony_ci configuration.report_dir: directory for output files 128a8e1175bSopenharmony_ci configuration.keep_all_reports: if false, delete old reports 129a8e1175bSopenharmony_ci configuration.brief: if true, output shorter report to stdout 130a8e1175bSopenharmony_ci configuration.check_abi: if true, compare ABIs 131a8e1175bSopenharmony_ci configuration.check_api: if true, compare APIs 132a8e1175bSopenharmony_ci configuration.check_storage: if true, compare storage format tests 133a8e1175bSopenharmony_ci configuration.skip_file: path to file containing symbols and types to skip 134a8e1175bSopenharmony_ci """ 135a8e1175bSopenharmony_ci self.repo_path = "." 136a8e1175bSopenharmony_ci self.log = None 137a8e1175bSopenharmony_ci self.verbose = configuration.verbose 138a8e1175bSopenharmony_ci self._setup_logger() 139a8e1175bSopenharmony_ci self.report_dir = os.path.abspath(configuration.report_dir) 140a8e1175bSopenharmony_ci self.keep_all_reports = configuration.keep_all_reports 141a8e1175bSopenharmony_ci self.can_remove_report_dir = not (os.path.exists(self.report_dir) or 142a8e1175bSopenharmony_ci self.keep_all_reports) 143a8e1175bSopenharmony_ci self.old_version = old_version 144a8e1175bSopenharmony_ci self.new_version = new_version 145a8e1175bSopenharmony_ci self.skip_file = configuration.skip_file 146a8e1175bSopenharmony_ci self.check_abi = configuration.check_abi 147a8e1175bSopenharmony_ci self.check_api = configuration.check_api 148a8e1175bSopenharmony_ci if self.check_abi != self.check_api: 149a8e1175bSopenharmony_ci raise Exception('Checking API without ABI or vice versa is not supported') 150a8e1175bSopenharmony_ci self.check_storage_tests = configuration.check_storage 151a8e1175bSopenharmony_ci self.brief = configuration.brief 152a8e1175bSopenharmony_ci self.git_command = "git" 153a8e1175bSopenharmony_ci self.make_command = "make" 154a8e1175bSopenharmony_ci 155a8e1175bSopenharmony_ci def _setup_logger(self): 156a8e1175bSopenharmony_ci self.log = logging.getLogger() 157a8e1175bSopenharmony_ci if self.verbose: 158a8e1175bSopenharmony_ci self.log.setLevel(logging.DEBUG) 159a8e1175bSopenharmony_ci else: 160a8e1175bSopenharmony_ci self.log.setLevel(logging.INFO) 161a8e1175bSopenharmony_ci self.log.addHandler(logging.StreamHandler()) 162a8e1175bSopenharmony_ci 163a8e1175bSopenharmony_ci @staticmethod 164a8e1175bSopenharmony_ci def check_abi_tools_are_installed(): 165a8e1175bSopenharmony_ci for command in ["abi-dumper", "abi-compliance-checker"]: 166a8e1175bSopenharmony_ci if not shutil.which(command): 167a8e1175bSopenharmony_ci raise Exception("{} not installed, aborting".format(command)) 168a8e1175bSopenharmony_ci 169a8e1175bSopenharmony_ci def _get_clean_worktree_for_git_revision(self, version): 170a8e1175bSopenharmony_ci """Make a separate worktree with version.revision checked out. 171a8e1175bSopenharmony_ci Do not modify the current worktree.""" 172a8e1175bSopenharmony_ci git_worktree_path = tempfile.mkdtemp() 173a8e1175bSopenharmony_ci if version.repository: 174a8e1175bSopenharmony_ci self.log.debug( 175a8e1175bSopenharmony_ci "Checking out git worktree for revision {} from {}".format( 176a8e1175bSopenharmony_ci version.revision, version.repository 177a8e1175bSopenharmony_ci ) 178a8e1175bSopenharmony_ci ) 179a8e1175bSopenharmony_ci fetch_output = subprocess.check_output( 180a8e1175bSopenharmony_ci [self.git_command, "fetch", 181a8e1175bSopenharmony_ci version.repository, version.revision], 182a8e1175bSopenharmony_ci cwd=self.repo_path, 183a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 184a8e1175bSopenharmony_ci ) 185a8e1175bSopenharmony_ci self.log.debug(fetch_output.decode("utf-8")) 186a8e1175bSopenharmony_ci worktree_rev = "FETCH_HEAD" 187a8e1175bSopenharmony_ci else: 188a8e1175bSopenharmony_ci self.log.debug("Checking out git worktree for revision {}".format( 189a8e1175bSopenharmony_ci version.revision 190a8e1175bSopenharmony_ci )) 191a8e1175bSopenharmony_ci worktree_rev = version.revision 192a8e1175bSopenharmony_ci worktree_output = subprocess.check_output( 193a8e1175bSopenharmony_ci [self.git_command, "worktree", "add", "--detach", 194a8e1175bSopenharmony_ci git_worktree_path, worktree_rev], 195a8e1175bSopenharmony_ci cwd=self.repo_path, 196a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 197a8e1175bSopenharmony_ci ) 198a8e1175bSopenharmony_ci self.log.debug(worktree_output.decode("utf-8")) 199a8e1175bSopenharmony_ci version.commit = subprocess.check_output( 200a8e1175bSopenharmony_ci [self.git_command, "rev-parse", "HEAD"], 201a8e1175bSopenharmony_ci cwd=git_worktree_path, 202a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 203a8e1175bSopenharmony_ci ).decode("ascii").rstrip() 204a8e1175bSopenharmony_ci self.log.debug("Commit is {}".format(version.commit)) 205a8e1175bSopenharmony_ci return git_worktree_path 206a8e1175bSopenharmony_ci 207a8e1175bSopenharmony_ci def _update_git_submodules(self, git_worktree_path, version): 208a8e1175bSopenharmony_ci """If the crypto submodule is present, initialize it. 209a8e1175bSopenharmony_ci if version.crypto_revision exists, update it to that revision, 210a8e1175bSopenharmony_ci otherwise update it to the default revision""" 211a8e1175bSopenharmony_ci update_output = subprocess.check_output( 212a8e1175bSopenharmony_ci [self.git_command, "submodule", "update", "--init", '--recursive'], 213a8e1175bSopenharmony_ci cwd=git_worktree_path, 214a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 215a8e1175bSopenharmony_ci ) 216a8e1175bSopenharmony_ci self.log.debug(update_output.decode("utf-8")) 217a8e1175bSopenharmony_ci if not (os.path.exists(os.path.join(git_worktree_path, "crypto")) 218a8e1175bSopenharmony_ci and version.crypto_revision): 219a8e1175bSopenharmony_ci return 220a8e1175bSopenharmony_ci 221a8e1175bSopenharmony_ci if version.crypto_repository: 222a8e1175bSopenharmony_ci fetch_output = subprocess.check_output( 223a8e1175bSopenharmony_ci [self.git_command, "fetch", version.crypto_repository, 224a8e1175bSopenharmony_ci version.crypto_revision], 225a8e1175bSopenharmony_ci cwd=os.path.join(git_worktree_path, "crypto"), 226a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 227a8e1175bSopenharmony_ci ) 228a8e1175bSopenharmony_ci self.log.debug(fetch_output.decode("utf-8")) 229a8e1175bSopenharmony_ci crypto_rev = "FETCH_HEAD" 230a8e1175bSopenharmony_ci else: 231a8e1175bSopenharmony_ci crypto_rev = version.crypto_revision 232a8e1175bSopenharmony_ci 233a8e1175bSopenharmony_ci checkout_output = subprocess.check_output( 234a8e1175bSopenharmony_ci [self.git_command, "checkout", crypto_rev], 235a8e1175bSopenharmony_ci cwd=os.path.join(git_worktree_path, "crypto"), 236a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 237a8e1175bSopenharmony_ci ) 238a8e1175bSopenharmony_ci self.log.debug(checkout_output.decode("utf-8")) 239a8e1175bSopenharmony_ci 240a8e1175bSopenharmony_ci def _build_shared_libraries(self, git_worktree_path, version): 241a8e1175bSopenharmony_ci """Build the shared libraries in the specified worktree.""" 242a8e1175bSopenharmony_ci my_environment = os.environ.copy() 243a8e1175bSopenharmony_ci my_environment["CFLAGS"] = "-g -Og" 244a8e1175bSopenharmony_ci my_environment["SHARED"] = "1" 245a8e1175bSopenharmony_ci if os.path.exists(os.path.join(git_worktree_path, "crypto")): 246a8e1175bSopenharmony_ci my_environment["USE_CRYPTO_SUBMODULE"] = "1" 247a8e1175bSopenharmony_ci make_output = subprocess.check_output( 248a8e1175bSopenharmony_ci [self.make_command, "lib"], 249a8e1175bSopenharmony_ci env=my_environment, 250a8e1175bSopenharmony_ci cwd=git_worktree_path, 251a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 252a8e1175bSopenharmony_ci ) 253a8e1175bSopenharmony_ci self.log.debug(make_output.decode("utf-8")) 254a8e1175bSopenharmony_ci for root, _dirs, files in os.walk(git_worktree_path): 255a8e1175bSopenharmony_ci for file in fnmatch.filter(files, "*.so"): 256a8e1175bSopenharmony_ci version.modules[os.path.splitext(file)[0]] = ( 257a8e1175bSopenharmony_ci os.path.join(root, file) 258a8e1175bSopenharmony_ci ) 259a8e1175bSopenharmony_ci 260a8e1175bSopenharmony_ci @staticmethod 261a8e1175bSopenharmony_ci def _pretty_revision(version): 262a8e1175bSopenharmony_ci if version.revision == version.commit: 263a8e1175bSopenharmony_ci return version.revision 264a8e1175bSopenharmony_ci else: 265a8e1175bSopenharmony_ci return "{} ({})".format(version.revision, version.commit) 266a8e1175bSopenharmony_ci 267a8e1175bSopenharmony_ci def _get_abi_dumps_from_shared_libraries(self, version): 268a8e1175bSopenharmony_ci """Generate the ABI dumps for the specified git revision. 269a8e1175bSopenharmony_ci The shared libraries must have been built and the module paths 270a8e1175bSopenharmony_ci present in version.modules.""" 271a8e1175bSopenharmony_ci for mbed_module, module_path in version.modules.items(): 272a8e1175bSopenharmony_ci output_path = os.path.join( 273a8e1175bSopenharmony_ci self.report_dir, "{}-{}-{}.dump".format( 274a8e1175bSopenharmony_ci mbed_module, version.revision, version.version 275a8e1175bSopenharmony_ci ) 276a8e1175bSopenharmony_ci ) 277a8e1175bSopenharmony_ci abi_dump_command = [ 278a8e1175bSopenharmony_ci "abi-dumper", 279a8e1175bSopenharmony_ci module_path, 280a8e1175bSopenharmony_ci "-o", output_path, 281a8e1175bSopenharmony_ci "-lver", self._pretty_revision(version), 282a8e1175bSopenharmony_ci ] 283a8e1175bSopenharmony_ci abi_dump_output = subprocess.check_output( 284a8e1175bSopenharmony_ci abi_dump_command, 285a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 286a8e1175bSopenharmony_ci ) 287a8e1175bSopenharmony_ci self.log.debug(abi_dump_output.decode("utf-8")) 288a8e1175bSopenharmony_ci version.abi_dumps[mbed_module] = output_path 289a8e1175bSopenharmony_ci 290a8e1175bSopenharmony_ci @staticmethod 291a8e1175bSopenharmony_ci def _normalize_storage_test_case_data(line): 292a8e1175bSopenharmony_ci """Eliminate cosmetic or irrelevant details in storage format test cases.""" 293a8e1175bSopenharmony_ci line = re.sub(r'\s+', r'', line) 294a8e1175bSopenharmony_ci return line 295a8e1175bSopenharmony_ci 296a8e1175bSopenharmony_ci def _read_storage_tests(self, 297a8e1175bSopenharmony_ci directory, 298a8e1175bSopenharmony_ci filename, 299a8e1175bSopenharmony_ci is_generated, 300a8e1175bSopenharmony_ci storage_tests): 301a8e1175bSopenharmony_ci """Record storage tests from the given file. 302a8e1175bSopenharmony_ci 303a8e1175bSopenharmony_ci Populate the storage_tests dictionary with test cases read from 304a8e1175bSopenharmony_ci filename under directory. 305a8e1175bSopenharmony_ci """ 306a8e1175bSopenharmony_ci at_paragraph_start = True 307a8e1175bSopenharmony_ci description = None 308a8e1175bSopenharmony_ci full_path = os.path.join(directory, filename) 309a8e1175bSopenharmony_ci with open(full_path) as fd: 310a8e1175bSopenharmony_ci for line_number, line in enumerate(fd, 1): 311a8e1175bSopenharmony_ci line = line.strip() 312a8e1175bSopenharmony_ci if not line: 313a8e1175bSopenharmony_ci at_paragraph_start = True 314a8e1175bSopenharmony_ci continue 315a8e1175bSopenharmony_ci if line.startswith('#'): 316a8e1175bSopenharmony_ci continue 317a8e1175bSopenharmony_ci if at_paragraph_start: 318a8e1175bSopenharmony_ci description = line.strip() 319a8e1175bSopenharmony_ci at_paragraph_start = False 320a8e1175bSopenharmony_ci continue 321a8e1175bSopenharmony_ci if line.startswith('depends_on:'): 322a8e1175bSopenharmony_ci continue 323a8e1175bSopenharmony_ci # We've reached a test case data line 324a8e1175bSopenharmony_ci test_case_data = self._normalize_storage_test_case_data(line) 325a8e1175bSopenharmony_ci if not is_generated: 326a8e1175bSopenharmony_ci # In manual test data, only look at read tests. 327a8e1175bSopenharmony_ci function_name = test_case_data.split(':', 1)[0] 328a8e1175bSopenharmony_ci if 'read' not in function_name.split('_'): 329a8e1175bSopenharmony_ci continue 330a8e1175bSopenharmony_ci metadata = SimpleNamespace( 331a8e1175bSopenharmony_ci filename=filename, 332a8e1175bSopenharmony_ci line_number=line_number, 333a8e1175bSopenharmony_ci description=description 334a8e1175bSopenharmony_ci ) 335a8e1175bSopenharmony_ci storage_tests[test_case_data] = metadata 336a8e1175bSopenharmony_ci 337a8e1175bSopenharmony_ci @staticmethod 338a8e1175bSopenharmony_ci def _list_generated_test_data_files(git_worktree_path): 339a8e1175bSopenharmony_ci """List the generated test data files.""" 340a8e1175bSopenharmony_ci output = subprocess.check_output( 341a8e1175bSopenharmony_ci ['tests/scripts/generate_psa_tests.py', '--list'], 342a8e1175bSopenharmony_ci cwd=git_worktree_path, 343a8e1175bSopenharmony_ci ).decode('ascii') 344a8e1175bSopenharmony_ci return [line for line in output.split('\n') if line] 345a8e1175bSopenharmony_ci 346a8e1175bSopenharmony_ci def _get_storage_format_tests(self, version, git_worktree_path): 347a8e1175bSopenharmony_ci """Record the storage format tests for the specified git version. 348a8e1175bSopenharmony_ci 349a8e1175bSopenharmony_ci The storage format tests are the test suite data files whose name 350a8e1175bSopenharmony_ci contains "storage_format". 351a8e1175bSopenharmony_ci 352a8e1175bSopenharmony_ci The version must be checked out at git_worktree_path. 353a8e1175bSopenharmony_ci 354a8e1175bSopenharmony_ci This function creates or updates the generated data files. 355a8e1175bSopenharmony_ci """ 356a8e1175bSopenharmony_ci # Existing test data files. This may be missing some automatically 357a8e1175bSopenharmony_ci # generated files if they haven't been generated yet. 358a8e1175bSopenharmony_ci storage_data_files = set(glob.glob( 359a8e1175bSopenharmony_ci 'tests/suites/test_suite_*storage_format*.data' 360a8e1175bSopenharmony_ci )) 361a8e1175bSopenharmony_ci # Discover and (re)generate automatically generated data files. 362a8e1175bSopenharmony_ci to_be_generated = set() 363a8e1175bSopenharmony_ci for filename in self._list_generated_test_data_files(git_worktree_path): 364a8e1175bSopenharmony_ci if 'storage_format' in filename: 365a8e1175bSopenharmony_ci storage_data_files.add(filename) 366a8e1175bSopenharmony_ci to_be_generated.add(filename) 367a8e1175bSopenharmony_ci subprocess.check_call( 368a8e1175bSopenharmony_ci ['tests/scripts/generate_psa_tests.py'] + sorted(to_be_generated), 369a8e1175bSopenharmony_ci cwd=git_worktree_path, 370a8e1175bSopenharmony_ci ) 371a8e1175bSopenharmony_ci for test_file in sorted(storage_data_files): 372a8e1175bSopenharmony_ci self._read_storage_tests(git_worktree_path, 373a8e1175bSopenharmony_ci test_file, 374a8e1175bSopenharmony_ci test_file in to_be_generated, 375a8e1175bSopenharmony_ci version.storage_tests) 376a8e1175bSopenharmony_ci 377a8e1175bSopenharmony_ci def _cleanup_worktree(self, git_worktree_path): 378a8e1175bSopenharmony_ci """Remove the specified git worktree.""" 379a8e1175bSopenharmony_ci shutil.rmtree(git_worktree_path) 380a8e1175bSopenharmony_ci worktree_output = subprocess.check_output( 381a8e1175bSopenharmony_ci [self.git_command, "worktree", "prune"], 382a8e1175bSopenharmony_ci cwd=self.repo_path, 383a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 384a8e1175bSopenharmony_ci ) 385a8e1175bSopenharmony_ci self.log.debug(worktree_output.decode("utf-8")) 386a8e1175bSopenharmony_ci 387a8e1175bSopenharmony_ci def _get_abi_dump_for_ref(self, version): 388a8e1175bSopenharmony_ci """Generate the interface information for the specified git revision.""" 389a8e1175bSopenharmony_ci git_worktree_path = self._get_clean_worktree_for_git_revision(version) 390a8e1175bSopenharmony_ci self._update_git_submodules(git_worktree_path, version) 391a8e1175bSopenharmony_ci if self.check_abi: 392a8e1175bSopenharmony_ci self._build_shared_libraries(git_worktree_path, version) 393a8e1175bSopenharmony_ci self._get_abi_dumps_from_shared_libraries(version) 394a8e1175bSopenharmony_ci if self.check_storage_tests: 395a8e1175bSopenharmony_ci self._get_storage_format_tests(version, git_worktree_path) 396a8e1175bSopenharmony_ci self._cleanup_worktree(git_worktree_path) 397a8e1175bSopenharmony_ci 398a8e1175bSopenharmony_ci def _remove_children_with_tag(self, parent, tag): 399a8e1175bSopenharmony_ci children = parent.getchildren() 400a8e1175bSopenharmony_ci for child in children: 401a8e1175bSopenharmony_ci if child.tag == tag: 402a8e1175bSopenharmony_ci parent.remove(child) 403a8e1175bSopenharmony_ci else: 404a8e1175bSopenharmony_ci self._remove_children_with_tag(child, tag) 405a8e1175bSopenharmony_ci 406a8e1175bSopenharmony_ci def _remove_extra_detail_from_report(self, report_root): 407a8e1175bSopenharmony_ci for tag in ['test_info', 'test_results', 'problem_summary', 408a8e1175bSopenharmony_ci 'added_symbols', 'affected']: 409a8e1175bSopenharmony_ci self._remove_children_with_tag(report_root, tag) 410a8e1175bSopenharmony_ci 411a8e1175bSopenharmony_ci for report in report_root: 412a8e1175bSopenharmony_ci for problems in report.getchildren()[:]: 413a8e1175bSopenharmony_ci if not problems.getchildren(): 414a8e1175bSopenharmony_ci report.remove(problems) 415a8e1175bSopenharmony_ci 416a8e1175bSopenharmony_ci def _abi_compliance_command(self, mbed_module, output_path): 417a8e1175bSopenharmony_ci """Build the command to run to analyze the library mbed_module. 418a8e1175bSopenharmony_ci The report will be placed in output_path.""" 419a8e1175bSopenharmony_ci abi_compliance_command = [ 420a8e1175bSopenharmony_ci "abi-compliance-checker", 421a8e1175bSopenharmony_ci "-l", mbed_module, 422a8e1175bSopenharmony_ci "-old", self.old_version.abi_dumps[mbed_module], 423a8e1175bSopenharmony_ci "-new", self.new_version.abi_dumps[mbed_module], 424a8e1175bSopenharmony_ci "-strict", 425a8e1175bSopenharmony_ci "-report-path", output_path, 426a8e1175bSopenharmony_ci ] 427a8e1175bSopenharmony_ci if self.skip_file: 428a8e1175bSopenharmony_ci abi_compliance_command += ["-skip-symbols", self.skip_file, 429a8e1175bSopenharmony_ci "-skip-types", self.skip_file] 430a8e1175bSopenharmony_ci if self.brief: 431a8e1175bSopenharmony_ci abi_compliance_command += ["-report-format", "xml", 432a8e1175bSopenharmony_ci "-stdout"] 433a8e1175bSopenharmony_ci return abi_compliance_command 434a8e1175bSopenharmony_ci 435a8e1175bSopenharmony_ci def _is_library_compatible(self, mbed_module, compatibility_report): 436a8e1175bSopenharmony_ci """Test if the library mbed_module has remained compatible. 437a8e1175bSopenharmony_ci Append a message regarding compatibility to compatibility_report.""" 438a8e1175bSopenharmony_ci output_path = os.path.join( 439a8e1175bSopenharmony_ci self.report_dir, "{}-{}-{}.html".format( 440a8e1175bSopenharmony_ci mbed_module, self.old_version.revision, 441a8e1175bSopenharmony_ci self.new_version.revision 442a8e1175bSopenharmony_ci ) 443a8e1175bSopenharmony_ci ) 444a8e1175bSopenharmony_ci try: 445a8e1175bSopenharmony_ci subprocess.check_output( 446a8e1175bSopenharmony_ci self._abi_compliance_command(mbed_module, output_path), 447a8e1175bSopenharmony_ci stderr=subprocess.STDOUT 448a8e1175bSopenharmony_ci ) 449a8e1175bSopenharmony_ci except subprocess.CalledProcessError as err: 450a8e1175bSopenharmony_ci if err.returncode != 1: 451a8e1175bSopenharmony_ci raise err 452a8e1175bSopenharmony_ci if self.brief: 453a8e1175bSopenharmony_ci self.log.info( 454a8e1175bSopenharmony_ci "Compatibility issues found for {}".format(mbed_module) 455a8e1175bSopenharmony_ci ) 456a8e1175bSopenharmony_ci report_root = ET.fromstring(err.output.decode("utf-8")) 457a8e1175bSopenharmony_ci self._remove_extra_detail_from_report(report_root) 458a8e1175bSopenharmony_ci self.log.info(ET.tostring(report_root).decode("utf-8")) 459a8e1175bSopenharmony_ci else: 460a8e1175bSopenharmony_ci self.can_remove_report_dir = False 461a8e1175bSopenharmony_ci compatibility_report.append( 462a8e1175bSopenharmony_ci "Compatibility issues found for {}, " 463a8e1175bSopenharmony_ci "for details see {}".format(mbed_module, output_path) 464a8e1175bSopenharmony_ci ) 465a8e1175bSopenharmony_ci return False 466a8e1175bSopenharmony_ci compatibility_report.append( 467a8e1175bSopenharmony_ci "No compatibility issues for {}".format(mbed_module) 468a8e1175bSopenharmony_ci ) 469a8e1175bSopenharmony_ci if not (self.keep_all_reports or self.brief): 470a8e1175bSopenharmony_ci os.remove(output_path) 471a8e1175bSopenharmony_ci return True 472a8e1175bSopenharmony_ci 473a8e1175bSopenharmony_ci @staticmethod 474a8e1175bSopenharmony_ci def _is_storage_format_compatible(old_tests, new_tests, 475a8e1175bSopenharmony_ci compatibility_report): 476a8e1175bSopenharmony_ci """Check whether all tests present in old_tests are also in new_tests. 477a8e1175bSopenharmony_ci 478a8e1175bSopenharmony_ci Append a message regarding compatibility to compatibility_report. 479a8e1175bSopenharmony_ci """ 480a8e1175bSopenharmony_ci missing = frozenset(old_tests.keys()).difference(new_tests.keys()) 481a8e1175bSopenharmony_ci for test_data in sorted(missing): 482a8e1175bSopenharmony_ci metadata = old_tests[test_data] 483a8e1175bSopenharmony_ci compatibility_report.append( 484a8e1175bSopenharmony_ci 'Test case from {} line {} "{}" has disappeared: {}'.format( 485a8e1175bSopenharmony_ci metadata.filename, metadata.line_number, 486a8e1175bSopenharmony_ci metadata.description, test_data 487a8e1175bSopenharmony_ci ) 488a8e1175bSopenharmony_ci ) 489a8e1175bSopenharmony_ci compatibility_report.append( 490a8e1175bSopenharmony_ci 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format( 491a8e1175bSopenharmony_ci len(missing), len(old_tests) 492a8e1175bSopenharmony_ci ) if missing else 493a8e1175bSopenharmony_ci 'PASS: All {} storage format test cases are preserved.'.format( 494a8e1175bSopenharmony_ci len(old_tests) 495a8e1175bSopenharmony_ci ) 496a8e1175bSopenharmony_ci ) 497a8e1175bSopenharmony_ci compatibility_report.append( 498a8e1175bSopenharmony_ci 'Info: number of storage format tests cases: {} -> {}.'.format( 499a8e1175bSopenharmony_ci len(old_tests), len(new_tests) 500a8e1175bSopenharmony_ci ) 501a8e1175bSopenharmony_ci ) 502a8e1175bSopenharmony_ci return not missing 503a8e1175bSopenharmony_ci 504a8e1175bSopenharmony_ci def get_abi_compatibility_report(self): 505a8e1175bSopenharmony_ci """Generate a report of the differences between the reference ABI 506a8e1175bSopenharmony_ci and the new ABI. ABI dumps from self.old_version and self.new_version 507a8e1175bSopenharmony_ci must be available.""" 508a8e1175bSopenharmony_ci compatibility_report = ["Checking evolution from {} to {}".format( 509a8e1175bSopenharmony_ci self._pretty_revision(self.old_version), 510a8e1175bSopenharmony_ci self._pretty_revision(self.new_version) 511a8e1175bSopenharmony_ci )] 512a8e1175bSopenharmony_ci compliance_return_code = 0 513a8e1175bSopenharmony_ci 514a8e1175bSopenharmony_ci if self.check_abi: 515a8e1175bSopenharmony_ci shared_modules = list(set(self.old_version.modules.keys()) & 516a8e1175bSopenharmony_ci set(self.new_version.modules.keys())) 517a8e1175bSopenharmony_ci for mbed_module in shared_modules: 518a8e1175bSopenharmony_ci if not self._is_library_compatible(mbed_module, 519a8e1175bSopenharmony_ci compatibility_report): 520a8e1175bSopenharmony_ci compliance_return_code = 1 521a8e1175bSopenharmony_ci 522a8e1175bSopenharmony_ci if self.check_storage_tests: 523a8e1175bSopenharmony_ci if not self._is_storage_format_compatible( 524a8e1175bSopenharmony_ci self.old_version.storage_tests, 525a8e1175bSopenharmony_ci self.new_version.storage_tests, 526a8e1175bSopenharmony_ci compatibility_report): 527a8e1175bSopenharmony_ci compliance_return_code = 1 528a8e1175bSopenharmony_ci 529a8e1175bSopenharmony_ci for version in [self.old_version, self.new_version]: 530a8e1175bSopenharmony_ci for mbed_module, mbed_module_dump in version.abi_dumps.items(): 531a8e1175bSopenharmony_ci os.remove(mbed_module_dump) 532a8e1175bSopenharmony_ci if self.can_remove_report_dir: 533a8e1175bSopenharmony_ci os.rmdir(self.report_dir) 534a8e1175bSopenharmony_ci self.log.info("\n".join(compatibility_report)) 535a8e1175bSopenharmony_ci return compliance_return_code 536a8e1175bSopenharmony_ci 537a8e1175bSopenharmony_ci def check_for_abi_changes(self): 538a8e1175bSopenharmony_ci """Generate a report of ABI differences 539a8e1175bSopenharmony_ci between self.old_rev and self.new_rev.""" 540a8e1175bSopenharmony_ci build_tree.check_repo_path() 541a8e1175bSopenharmony_ci if self.check_api or self.check_abi: 542a8e1175bSopenharmony_ci self.check_abi_tools_are_installed() 543a8e1175bSopenharmony_ci self._get_abi_dump_for_ref(self.old_version) 544a8e1175bSopenharmony_ci self._get_abi_dump_for_ref(self.new_version) 545a8e1175bSopenharmony_ci return self.get_abi_compatibility_report() 546a8e1175bSopenharmony_ci 547a8e1175bSopenharmony_ci 548a8e1175bSopenharmony_cidef run_main(): 549a8e1175bSopenharmony_ci try: 550a8e1175bSopenharmony_ci parser = argparse.ArgumentParser( 551a8e1175bSopenharmony_ci description=__doc__ 552a8e1175bSopenharmony_ci ) 553a8e1175bSopenharmony_ci parser.add_argument( 554a8e1175bSopenharmony_ci "-v", "--verbose", action="store_true", 555a8e1175bSopenharmony_ci help="set verbosity level", 556a8e1175bSopenharmony_ci ) 557a8e1175bSopenharmony_ci parser.add_argument( 558a8e1175bSopenharmony_ci "-r", "--report-dir", type=str, default="reports", 559a8e1175bSopenharmony_ci help="directory where reports are stored, default is reports", 560a8e1175bSopenharmony_ci ) 561a8e1175bSopenharmony_ci parser.add_argument( 562a8e1175bSopenharmony_ci "-k", "--keep-all-reports", action="store_true", 563a8e1175bSopenharmony_ci help="keep all reports, even if there are no compatibility issues", 564a8e1175bSopenharmony_ci ) 565a8e1175bSopenharmony_ci parser.add_argument( 566a8e1175bSopenharmony_ci "-o", "--old-rev", type=str, help="revision for old version.", 567a8e1175bSopenharmony_ci required=True, 568a8e1175bSopenharmony_ci ) 569a8e1175bSopenharmony_ci parser.add_argument( 570a8e1175bSopenharmony_ci "-or", "--old-repo", type=str, help="repository for old version." 571a8e1175bSopenharmony_ci ) 572a8e1175bSopenharmony_ci parser.add_argument( 573a8e1175bSopenharmony_ci "-oc", "--old-crypto-rev", type=str, 574a8e1175bSopenharmony_ci help="revision for old crypto submodule." 575a8e1175bSopenharmony_ci ) 576a8e1175bSopenharmony_ci parser.add_argument( 577a8e1175bSopenharmony_ci "-ocr", "--old-crypto-repo", type=str, 578a8e1175bSopenharmony_ci help="repository for old crypto submodule." 579a8e1175bSopenharmony_ci ) 580a8e1175bSopenharmony_ci parser.add_argument( 581a8e1175bSopenharmony_ci "-n", "--new-rev", type=str, help="revision for new version", 582a8e1175bSopenharmony_ci required=True, 583a8e1175bSopenharmony_ci ) 584a8e1175bSopenharmony_ci parser.add_argument( 585a8e1175bSopenharmony_ci "-nr", "--new-repo", type=str, help="repository for new version." 586a8e1175bSopenharmony_ci ) 587a8e1175bSopenharmony_ci parser.add_argument( 588a8e1175bSopenharmony_ci "-nc", "--new-crypto-rev", type=str, 589a8e1175bSopenharmony_ci help="revision for new crypto version" 590a8e1175bSopenharmony_ci ) 591a8e1175bSopenharmony_ci parser.add_argument( 592a8e1175bSopenharmony_ci "-ncr", "--new-crypto-repo", type=str, 593a8e1175bSopenharmony_ci help="repository for new crypto submodule." 594a8e1175bSopenharmony_ci ) 595a8e1175bSopenharmony_ci parser.add_argument( 596a8e1175bSopenharmony_ci "-s", "--skip-file", type=str, 597a8e1175bSopenharmony_ci help=("path to file containing symbols and types to skip " 598a8e1175bSopenharmony_ci "(typically \"-s identifiers\" after running " 599a8e1175bSopenharmony_ci "\"tests/scripts/list-identifiers.sh --internal\")") 600a8e1175bSopenharmony_ci ) 601a8e1175bSopenharmony_ci parser.add_argument( 602a8e1175bSopenharmony_ci "--check-abi", 603a8e1175bSopenharmony_ci action='store_true', default=True, 604a8e1175bSopenharmony_ci help="Perform ABI comparison (default: yes)" 605a8e1175bSopenharmony_ci ) 606a8e1175bSopenharmony_ci parser.add_argument("--no-check-abi", action='store_false', dest='check_abi') 607a8e1175bSopenharmony_ci parser.add_argument( 608a8e1175bSopenharmony_ci "--check-api", 609a8e1175bSopenharmony_ci action='store_true', default=True, 610a8e1175bSopenharmony_ci help="Perform API comparison (default: yes)" 611a8e1175bSopenharmony_ci ) 612a8e1175bSopenharmony_ci parser.add_argument("--no-check-api", action='store_false', dest='check_api') 613a8e1175bSopenharmony_ci parser.add_argument( 614a8e1175bSopenharmony_ci "--check-storage", 615a8e1175bSopenharmony_ci action='store_true', default=True, 616a8e1175bSopenharmony_ci help="Perform storage tests comparison (default: yes)" 617a8e1175bSopenharmony_ci ) 618a8e1175bSopenharmony_ci parser.add_argument("--no-check-storage", action='store_false', dest='check_storage') 619a8e1175bSopenharmony_ci parser.add_argument( 620a8e1175bSopenharmony_ci "-b", "--brief", action="store_true", 621a8e1175bSopenharmony_ci help="output only the list of issues to stdout, instead of a full report", 622a8e1175bSopenharmony_ci ) 623a8e1175bSopenharmony_ci abi_args = parser.parse_args() 624a8e1175bSopenharmony_ci if os.path.isfile(abi_args.report_dir): 625a8e1175bSopenharmony_ci print("Error: {} is not a directory".format(abi_args.report_dir)) 626a8e1175bSopenharmony_ci parser.exit() 627a8e1175bSopenharmony_ci old_version = SimpleNamespace( 628a8e1175bSopenharmony_ci version="old", 629a8e1175bSopenharmony_ci repository=abi_args.old_repo, 630a8e1175bSopenharmony_ci revision=abi_args.old_rev, 631a8e1175bSopenharmony_ci commit=None, 632a8e1175bSopenharmony_ci crypto_repository=abi_args.old_crypto_repo, 633a8e1175bSopenharmony_ci crypto_revision=abi_args.old_crypto_rev, 634a8e1175bSopenharmony_ci abi_dumps={}, 635a8e1175bSopenharmony_ci storage_tests={}, 636a8e1175bSopenharmony_ci modules={} 637a8e1175bSopenharmony_ci ) 638a8e1175bSopenharmony_ci new_version = SimpleNamespace( 639a8e1175bSopenharmony_ci version="new", 640a8e1175bSopenharmony_ci repository=abi_args.new_repo, 641a8e1175bSopenharmony_ci revision=abi_args.new_rev, 642a8e1175bSopenharmony_ci commit=None, 643a8e1175bSopenharmony_ci crypto_repository=abi_args.new_crypto_repo, 644a8e1175bSopenharmony_ci crypto_revision=abi_args.new_crypto_rev, 645a8e1175bSopenharmony_ci abi_dumps={}, 646a8e1175bSopenharmony_ci storage_tests={}, 647a8e1175bSopenharmony_ci modules={} 648a8e1175bSopenharmony_ci ) 649a8e1175bSopenharmony_ci configuration = SimpleNamespace( 650a8e1175bSopenharmony_ci verbose=abi_args.verbose, 651a8e1175bSopenharmony_ci report_dir=abi_args.report_dir, 652a8e1175bSopenharmony_ci keep_all_reports=abi_args.keep_all_reports, 653a8e1175bSopenharmony_ci brief=abi_args.brief, 654a8e1175bSopenharmony_ci check_abi=abi_args.check_abi, 655a8e1175bSopenharmony_ci check_api=abi_args.check_api, 656a8e1175bSopenharmony_ci check_storage=abi_args.check_storage, 657a8e1175bSopenharmony_ci skip_file=abi_args.skip_file 658a8e1175bSopenharmony_ci ) 659a8e1175bSopenharmony_ci abi_check = AbiChecker(old_version, new_version, configuration) 660a8e1175bSopenharmony_ci return_code = abi_check.check_for_abi_changes() 661a8e1175bSopenharmony_ci sys.exit(return_code) 662a8e1175bSopenharmony_ci except Exception: # pylint: disable=broad-except 663a8e1175bSopenharmony_ci # Print the backtrace and exit explicitly so as to exit with 664a8e1175bSopenharmony_ci # status 2, not 1. 665a8e1175bSopenharmony_ci traceback.print_exc() 666a8e1175bSopenharmony_ci sys.exit(2) 667a8e1175bSopenharmony_ci 668a8e1175bSopenharmony_ci 669a8e1175bSopenharmony_ciif __name__ == "__main__": 670a8e1175bSopenharmony_ci run_main() 671