diff options
| author | 2020-06-11 17:21:05 +0000 | |
|---|---|---|
| committer | 2020-06-11 17:21:05 +0000 | |
| commit | 52481b445ba541d03d6bed20b7a97f245772f30c (patch) | |
| tree | 837ba020cd710649d30fef29765490a3b3d66738 /cc/scriptlib | |
| parent | 9e90e61ffdbf1831efb5019d2e9151b45aa001ec (diff) | |
| parent | b858c6d49791bf1879983a7873c7d2a2224504cb (diff) | |
Merge "Add ndk api parser for ndk api coverage."
Diffstat (limited to 'cc/scriptlib')
| -rw-r--r-- | cc/scriptlib/Android.bp | 23 | ||||
| -rw-r--r-- | cc/scriptlib/__init__.py | 0 | ||||
| -rwxr-xr-x | cc/scriptlib/gen_stub_libs.py | 507 | ||||
| -rwxr-xr-x | cc/scriptlib/ndk_api_coverage_parser.py | 134 | ||||
| -rwxr-xr-x | cc/scriptlib/test_gen_stub_libs.py | 807 | ||||
| -rw-r--r-- | cc/scriptlib/test_ndk_api_coverage_parser.py | 66 |
6 files changed, 1537 insertions, 0 deletions
diff --git a/cc/scriptlib/Android.bp b/cc/scriptlib/Android.bp new file mode 100644 index 000000000..daebfe156 --- /dev/null +++ b/cc/scriptlib/Android.bp @@ -0,0 +1,23 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +python_test_host { + name: "test_ndk_api_coverage_parser", + main: "test_ndk_api_coverage_parser.py", + srcs: [ + "test_ndk_api_coverage_parser.py", + ], +} diff --git a/cc/scriptlib/__init__.py b/cc/scriptlib/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/cc/scriptlib/__init__.py diff --git a/cc/scriptlib/gen_stub_libs.py b/cc/scriptlib/gen_stub_libs.py new file mode 100755 index 000000000..d61dfbb07 --- /dev/null +++ b/cc/scriptlib/gen_stub_libs.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Generates source for stub shared libraries for the NDK.""" +import argparse +import json +import logging +import os +import re +import sys + + +ALL_ARCHITECTURES = ( + 'arm', + 'arm64', + 'x86', + 'x86_64', +) + + +# Arbitrary magic number. We use the same one in api-level.h for this purpose. +FUTURE_API_LEVEL = 10000 + + +def logger(): + """Return the main logger for this module.""" + return logging.getLogger(__name__) + + +def get_tags(line): + """Returns a list of all tags on this line.""" + _, _, all_tags = line.strip().partition('#') + return [e for e in re.split(r'\s+', all_tags) if e.strip()] + + +def is_api_level_tag(tag): + """Returns true if this tag has an API level that may need decoding.""" + if tag.startswith('introduced='): + return True + if tag.startswith('introduced-'): + return True + if tag.startswith('versioned='): + return True + return False + + +def decode_api_level_tags(tags, api_map): + """Decodes API level code names in a list of tags. + + Raises: + ParseError: An unknown version name was found in a tag. + """ + for idx, tag in enumerate(tags): + if not is_api_level_tag(tag): + continue + name, value = split_tag(tag) + + try: + decoded = str(decode_api_level(value, api_map)) + tags[idx] = '='.join([name, decoded]) + except KeyError: + raise ParseError('Unknown version name in tag: {}'.format(tag)) + return tags + + +def split_tag(tag): + """Returns a key/value tuple of the tag. + + Raises: + ValueError: Tag is not a key/value type tag. + + Returns: Tuple of (key, value) of the tag. Both components are strings. + """ + if '=' not in tag: + raise ValueError('Not a key/value tag: ' + tag) + key, _, value = tag.partition('=') + return key, value + + +def get_tag_value(tag): + """Returns the value of a key/value tag. + + Raises: + ValueError: Tag is not a key/value type tag. + + Returns: Value part of tag as a string. + """ + return split_tag(tag)[1] + + +def version_is_private(version): + """Returns True if the version name should be treated as private.""" + return version.endswith('_PRIVATE') or version.endswith('_PLATFORM') + + +def should_omit_version(version, arch, api, llndk, apex): + """Returns True if the version section should be ommitted. + + We want to omit any sections that do not have any symbols we'll have in the + stub library. Sections that contain entirely future symbols or only symbols + for certain architectures. + """ + if version_is_private(version.name): + return True + if 'platform-only' in version.tags: + return True + + no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags + keep = no_llndk_no_apex or \ + ('llndk' in version.tags and llndk) or \ + ('apex' in version.tags and apex) + if not keep: + return True + if not symbol_in_arch(version.tags, arch): + return True + if not symbol_in_api(version.tags, arch, api): + return True + return False + + +def should_omit_symbol(symbol, arch, api, llndk, apex): + """Returns True if the symbol should be omitted.""" + no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags + keep = no_llndk_no_apex or \ + ('llndk' in symbol.tags and llndk) or \ + ('apex' in symbol.tags and apex) + if not keep: + return True + if not symbol_in_arch(symbol.tags, arch): + return True + if not symbol_in_api(symbol.tags, arch, api): + return True + return False + + +def symbol_in_arch(tags, arch): + """Returns true if the symbol is present for the given architecture.""" + has_arch_tags = False + for tag in tags: + if tag == arch: + return True + if tag in ALL_ARCHITECTURES: + has_arch_tags = True + + # If there were no arch tags, the symbol is available for all + # architectures. If there were any arch tags, the symbol is only available + # for the tagged architectures. + return not has_arch_tags + + +def symbol_in_api(tags, arch, api): + """Returns true if the symbol is present for the given API level.""" + introduced_tag = None + arch_specific = False + for tag in tags: + # If there is an arch-specific tag, it should override the common one. + if tag.startswith('introduced=') and not arch_specific: + introduced_tag = tag + elif tag.startswith('introduced-' + arch + '='): + introduced_tag = tag + arch_specific = True + elif tag == 'future': + return api == FUTURE_API_LEVEL + + if introduced_tag is None: + # We found no "introduced" tags, so the symbol has always been + # available. + return True + + return api >= int(get_tag_value(introduced_tag)) + + +def symbol_versioned_in_api(tags, api): + """Returns true if the symbol should be versioned for the given API. + + This models the `versioned=API` tag. This should be a very uncommonly + needed tag, and is really only needed to fix versioning mistakes that are + already out in the wild. + + For example, some of libc's __aeabi_* functions were originally placed in + the private version, but that was incorrect. They are now in LIBC_N, but + when building against any version prior to N we need the symbol to be + unversioned (otherwise it won't resolve on M where it is private). + """ + for tag in tags: + if tag.startswith('versioned='): + return api >= int(get_tag_value(tag)) + # If there is no "versioned" tag, the tag has been versioned for as long as + # it was introduced. + return True + + +class ParseError(RuntimeError): + """An error that occurred while parsing a symbol file.""" + pass + + +class MultiplyDefinedSymbolError(RuntimeError): + """A symbol name was multiply defined.""" + def __init__(self, multiply_defined_symbols): + super(MultiplyDefinedSymbolError, self).__init__( + 'Version script contains multiple definitions for: {}'.format( + ', '.join(multiply_defined_symbols))) + self.multiply_defined_symbols = multiply_defined_symbols + + +class Version(object): + """A version block of a symbol file.""" + def __init__(self, name, base, tags, symbols): + self.name = name + self.base = base + self.tags = tags + self.symbols = symbols + + def __eq__(self, other): + if self.name != other.name: + return False + if self.base != other.base: + return False + if self.tags != other.tags: + return False + if self.symbols != other.symbols: + return False + return True + + +class Symbol(object): + """A symbol definition from a symbol file.""" + def __init__(self, name, tags): + self.name = name + self.tags = tags + + def __eq__(self, other): + return self.name == other.name and set(self.tags) == set(other.tags) + + +class SymbolFileParser(object): + """Parses NDK symbol files.""" + def __init__(self, input_file, api_map, arch, api, llndk, apex): + self.input_file = input_file + self.api_map = api_map + self.arch = arch + self.api = api + self.llndk = llndk + self.apex = apex + self.current_line = None + + def parse(self): + """Parses the symbol file and returns a list of Version objects.""" + versions = [] + while self.next_line() != '': + if '{' in self.current_line: + versions.append(self.parse_version()) + else: + raise ParseError( + 'Unexpected contents at top level: ' + self.current_line) + + self.check_no_duplicate_symbols(versions) + return versions + + def check_no_duplicate_symbols(self, versions): + """Raises errors for multiply defined symbols. + + This situation is the normal case when symbol versioning is actually + used, but this script doesn't currently handle that. The error message + will be a not necessarily obvious "error: redefition of 'foo'" from + stub.c, so it's better for us to catch this situation and raise a + better error. + """ + symbol_names = set() + multiply_defined_symbols = set() + for version in versions: + if should_omit_version(version, self.arch, self.api, self.llndk, self.apex): + continue + + for symbol in version.symbols: + if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex): + continue + + if symbol.name in symbol_names: + multiply_defined_symbols.add(symbol.name) + symbol_names.add(symbol.name) + if multiply_defined_symbols: + raise MultiplyDefinedSymbolError( + sorted(list(multiply_defined_symbols))) + + def parse_version(self): + """Parses a single version section and returns a Version object.""" + name = self.current_line.split('{')[0].strip() + tags = get_tags(self.current_line) + tags = decode_api_level_tags(tags, self.api_map) + symbols = [] + global_scope = True + cpp_symbols = False + while self.next_line() != '': + if '}' in self.current_line: + # Line is something like '} BASE; # tags'. Both base and tags + # are optional here. + base = self.current_line.partition('}')[2] + base = base.partition('#')[0].strip() + if not base.endswith(';'): + raise ParseError( + 'Unterminated version/export "C++" block (expected ;).') + if cpp_symbols: + cpp_symbols = False + else: + base = base.rstrip(';').rstrip() + if base == '': + base = None + return Version(name, base, tags, symbols) + elif 'extern "C++" {' in self.current_line: + cpp_symbols = True + elif not cpp_symbols and ':' in self.current_line: + visibility = self.current_line.split(':')[0].strip() + if visibility == 'local': + global_scope = False + elif visibility == 'global': + global_scope = True + else: + raise ParseError('Unknown visiblity label: ' + visibility) + elif global_scope and not cpp_symbols: + symbols.append(self.parse_symbol()) + else: + # We're in a hidden scope or in 'extern "C++"' block. Ignore + # everything. + pass + raise ParseError('Unexpected EOF in version block.') + + def parse_symbol(self): + """Parses a single symbol line and returns a Symbol object.""" + if ';' not in self.current_line: + raise ParseError( + 'Expected ; to terminate symbol: ' + self.current_line) + if '*' in self.current_line: + raise ParseError( + 'Wildcard global symbols are not permitted.') + # Line is now in the format "<symbol-name>; # tags" + name, _, _ = self.current_line.strip().partition(';') + tags = get_tags(self.current_line) + tags = decode_api_level_tags(tags, self.api_map) + return Symbol(name, tags) + + def next_line(self): + """Returns the next non-empty non-comment line. + + A return value of '' indicates EOF. + """ + line = self.input_file.readline() + while line.strip() == '' or line.strip().startswith('#'): + line = self.input_file.readline() + + # We want to skip empty lines, but '' indicates EOF. + if line == '': + break + self.current_line = line + return self.current_line + + +class Generator(object): + """Output generator that writes stub source files and version scripts.""" + def __init__(self, src_file, version_script, arch, api, llndk, apex): + self.src_file = src_file + self.version_script = version_script + self.arch = arch + self.api = api + self.llndk = llndk + self.apex = apex + + def write(self, versions): + """Writes all symbol data to the output files.""" + for version in versions: + self.write_version(version) + + def write_version(self, version): + """Writes a single version block's data to the output files.""" + if should_omit_version(version, self.arch, self.api, self.llndk, self.apex): + return + + section_versioned = symbol_versioned_in_api(version.tags, self.api) + version_empty = True + pruned_symbols = [] + for symbol in version.symbols: + if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex): + continue + + if symbol_versioned_in_api(symbol.tags, self.api): + version_empty = False + pruned_symbols.append(symbol) + + if len(pruned_symbols) > 0: + if not version_empty and section_versioned: + self.version_script.write(version.name + ' {\n') + self.version_script.write(' global:\n') + for symbol in pruned_symbols: + emit_version = symbol_versioned_in_api(symbol.tags, self.api) + if section_versioned and emit_version: + self.version_script.write(' ' + symbol.name + ';\n') + + weak = '' + if 'weak' in symbol.tags: + weak = '__attribute__((weak)) ' + + if 'var' in symbol.tags: + self.src_file.write('{}int {} = 0;\n'.format( + weak, symbol.name)) + else: + self.src_file.write('{}void {}() {{}}\n'.format( + weak, symbol.name)) + + if not version_empty and section_versioned: + base = '' if version.base is None else ' ' + version.base + self.version_script.write('}' + base + ';\n') + + +def decode_api_level(api, api_map): + """Decodes the API level argument into the API level number. + + For the average case, this just decodes the integer value from the string, + but for unreleased APIs we need to translate from the API codename (like + "O") to the future API level for that codename. + """ + try: + return int(api) + except ValueError: + pass + + if api == "current": + return FUTURE_API_LEVEL + + return api_map[api] + + +def parse_args(): + """Parses and returns command line arguments.""" + parser = argparse.ArgumentParser() + + parser.add_argument('-v', '--verbose', action='count', default=0) + + parser.add_argument( + '--api', required=True, help='API level being targeted.') + parser.add_argument( + '--arch', choices=ALL_ARCHITECTURES, required=True, + help='Architecture being targeted.') + parser.add_argument( + '--llndk', action='store_true', help='Use the LLNDK variant.') + parser.add_argument( + '--apex', action='store_true', help='Use the APEX variant.') + + parser.add_argument( + '--api-map', type=os.path.realpath, required=True, + help='Path to the API level map JSON file.') + + parser.add_argument( + 'symbol_file', type=os.path.realpath, help='Path to symbol file.') + parser.add_argument( + 'stub_src', type=os.path.realpath, + help='Path to output stub source file.') + parser.add_argument( + 'version_script', type=os.path.realpath, + help='Path to output version script.') + + return parser.parse_args() + + +def main(): + """Program entry point.""" + args = parse_args() + + with open(args.api_map) as map_file: + api_map = json.load(map_file) + api = decode_api_level(args.api, api_map) + + verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG) + verbosity = args.verbose + if verbosity > 2: + verbosity = 2 + logging.basicConfig(level=verbose_map[verbosity]) + + with open(args.symbol_file) as symbol_file: + try: + versions = SymbolFileParser(symbol_file, api_map, args.arch, api, + args.llndk, args.apex).parse() + except MultiplyDefinedSymbolError as ex: + sys.exit('{}: error: {}'.format(args.symbol_file, ex)) + + with open(args.stub_src, 'w') as src_file: + with open(args.version_script, 'w') as version_file: + generator = Generator(src_file, version_file, args.arch, api, + args.llndk, args.apex) + generator.write(versions) + + +if __name__ == '__main__': + main() diff --git a/cc/scriptlib/ndk_api_coverage_parser.py b/cc/scriptlib/ndk_api_coverage_parser.py new file mode 100755 index 000000000..d74035b2a --- /dev/null +++ b/cc/scriptlib/ndk_api_coverage_parser.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# +# Copyright (C) 2020 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Generates xml of NDK libraries used for API coverage analysis.""" +import argparse +import json +import os +import sys + +from xml.etree.ElementTree import Element, SubElement, tostring +from gen_stub_libs import ALL_ARCHITECTURES, FUTURE_API_LEVEL, MultiplyDefinedSymbolError, SymbolFileParser + + +ROOT_ELEMENT_TAG = 'ndk-library' +SYMBOL_ELEMENT_TAG = 'symbol' +ARCHITECTURE_ATTRIBUTE_KEY = 'arch' +DEPRECATED_ATTRIBUTE_KEY = 'is_deprecated' +PLATFORM_ATTRIBUTE_KEY = 'is_platform' +NAME_ATTRIBUTE_KEY = 'name' +VARIABLE_TAG = 'var' +EXPOSED_TARGET_TAGS = ( + 'vndk', + 'apex', + 'llndk', +) +API_LEVEL_TAG_PREFIXES = ( + 'introduced=', + 'introduced-', +) + + +def parse_tags(tags): + """Parses tags and save needed tags in the created attributes. + + Return attributes dictionary. + """ + attributes = {} + arch = [] + for tag in tags: + if tag.startswith(tuple(API_LEVEL_TAG_PREFIXES)): + key, _, value = tag.partition('=') + attributes.update({key: value}) + elif tag in ALL_ARCHITECTURES: + arch.append(tag) + elif tag in EXPOSED_TARGET_TAGS: + attributes.update({tag: 'True'}) + attributes.update({ARCHITECTURE_ATTRIBUTE_KEY: ','.join(arch)}) + return attributes + + +class XmlGenerator(object): + """Output generator that writes parsed symbol file to a xml file.""" + def __init__(self, output_file): + self.output_file = output_file + + def convertToXml(self, versions): + """Writes all symbol data to the output file.""" + root = Element(ROOT_ELEMENT_TAG) + for version in versions: + if VARIABLE_TAG in version.tags: + continue + version_attributes = parse_tags(version.tags) + _, _, postfix = version.name.partition('_') + is_platform = postfix == 'PRIVATE' or postfix == 'PLATFORM' + is_deprecated = postfix == 'DEPRECATED' + version_attributes.update({PLATFORM_ATTRIBUTE_KEY: str(is_platform)}) + version_attributes.update({DEPRECATED_ATTRIBUTE_KEY: str(is_deprecated)}) + for symbol in version.symbols: + if VARIABLE_TAG in symbol.tags: + continue + attributes = {NAME_ATTRIBUTE_KEY: symbol.name} + attributes.update(version_attributes) + # If same version tags already exist, it will be overwrite here. + attributes.update(parse_tags(symbol.tags)) + SubElement(root, SYMBOL_ELEMENT_TAG, attributes) + return root + + def write_xml_to_file(self, root): + """Write xml element root to output_file.""" + parsed_data = tostring(root) + output_file = open(self.output_file, "wb") + output_file.write(parsed_data) + + def write(self, versions): + root = self.convertToXml(versions) + self.write_xml_to_file(root) + + +def parse_args(): + """Parses and returns command line arguments.""" + parser = argparse.ArgumentParser() + + parser.add_argument('symbol_file', type=os.path.realpath, help='Path to symbol file.') + parser.add_argument( + 'output_file', type=os.path.realpath, + help='The output parsed api coverage file.') + parser.add_argument( + '--api-map', type=os.path.realpath, required=True, + help='Path to the API level map JSON file.') + return parser.parse_args() + + +def main(): + """Program entry point.""" + args = parse_args() + + with open(args.api_map) as map_file: + api_map = json.load(map_file) + + with open(args.symbol_file) as symbol_file: + try: + versions = SymbolFileParser(symbol_file, api_map, "", FUTURE_API_LEVEL, + True, True).parse() + except MultiplyDefinedSymbolError as ex: + sys.exit('{}: error: {}'.format(args.symbol_file, ex)) + + generator = XmlGenerator(args.output_file) + generator.write(versions) + +if __name__ == '__main__': + main() diff --git a/cc/scriptlib/test_gen_stub_libs.py b/cc/scriptlib/test_gen_stub_libs.py new file mode 100755 index 000000000..0b45e7110 --- /dev/null +++ b/cc/scriptlib/test_gen_stub_libs.py @@ -0,0 +1,807 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for gen_stub_libs.py.""" +import io +import textwrap +import unittest + +import gen_stub_libs as gsl + + +# pylint: disable=missing-docstring + + +class DecodeApiLevelTest(unittest.TestCase): + def test_decode_api_level(self): + self.assertEqual(9, gsl.decode_api_level('9', {})) + self.assertEqual(9000, gsl.decode_api_level('O', {'O': 9000})) + + with self.assertRaises(KeyError): + gsl.decode_api_level('O', {}) + + +class TagsTest(unittest.TestCase): + def test_get_tags_no_tags(self): + self.assertEqual([], gsl.get_tags('')) + self.assertEqual([], gsl.get_tags('foo bar baz')) + + def test_get_tags(self): + self.assertEqual(['foo', 'bar'], gsl.get_tags('# foo bar')) + self.assertEqual(['bar', 'baz'], gsl.get_tags('foo # bar baz')) + + def test_split_tag(self): + self.assertTupleEqual(('foo', 'bar'), gsl.split_tag('foo=bar')) + self.assertTupleEqual(('foo', 'bar=baz'), gsl.split_tag('foo=bar=baz')) + with self.assertRaises(ValueError): + gsl.split_tag('foo') + + def test_get_tag_value(self): + self.assertEqual('bar', gsl.get_tag_value('foo=bar')) + self.assertEqual('bar=baz', gsl.get_tag_value('foo=bar=baz')) + with self.assertRaises(ValueError): + gsl.get_tag_value('foo') + + def test_is_api_level_tag(self): + self.assertTrue(gsl.is_api_level_tag('introduced=24')) + self.assertTrue(gsl.is_api_level_tag('introduced-arm=24')) + self.assertTrue(gsl.is_api_level_tag('versioned=24')) + + # Shouldn't try to process things that aren't a key/value tag. + self.assertFalse(gsl.is_api_level_tag('arm')) + self.assertFalse(gsl.is_api_level_tag('introduced')) + self.assertFalse(gsl.is_api_level_tag('versioned')) + + # We don't support arch specific `versioned` tags. + self.assertFalse(gsl.is_api_level_tag('versioned-arm=24')) + + def test_decode_api_level_tags(self): + api_map = { + 'O': 9000, + 'P': 9001, + } + + tags = [ + 'introduced=9', + 'introduced-arm=14', + 'versioned=16', + 'arm', + 'introduced=O', + 'introduced=P', + ] + expected_tags = [ + 'introduced=9', + 'introduced-arm=14', + 'versioned=16', + 'arm', + 'introduced=9000', + 'introduced=9001', + ] + self.assertListEqual( + expected_tags, gsl.decode_api_level_tags(tags, api_map)) + + with self.assertRaises(gsl.ParseError): + gsl.decode_api_level_tags(['introduced=O'], {}) + + +class PrivateVersionTest(unittest.TestCase): + def test_version_is_private(self): + self.assertFalse(gsl.version_is_private('foo')) + self.assertFalse(gsl.version_is_private('PRIVATE')) + self.assertFalse(gsl.version_is_private('PLATFORM')) + self.assertFalse(gsl.version_is_private('foo_private')) + self.assertFalse(gsl.version_is_private('foo_platform')) + self.assertFalse(gsl.version_is_private('foo_PRIVATE_')) + self.assertFalse(gsl.version_is_private('foo_PLATFORM_')) + + self.assertTrue(gsl.version_is_private('foo_PRIVATE')) + self.assertTrue(gsl.version_is_private('foo_PLATFORM')) + + +class SymbolPresenceTest(unittest.TestCase): + def test_symbol_in_arch(self): + self.assertTrue(gsl.symbol_in_arch([], 'arm')) + self.assertTrue(gsl.symbol_in_arch(['arm'], 'arm')) + + self.assertFalse(gsl.symbol_in_arch(['x86'], 'arm')) + + def test_symbol_in_api(self): + self.assertTrue(gsl.symbol_in_api([], 'arm', 9)) + self.assertTrue(gsl.symbol_in_api(['introduced=9'], 'arm', 9)) + self.assertTrue(gsl.symbol_in_api(['introduced=9'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api(['introduced-arm=9'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api(['introduced-arm=9'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api(['introduced-x86=14'], 'arm', 9)) + self.assertTrue(gsl.symbol_in_api( + ['introduced-arm=9', 'introduced-x86=21'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api( + ['introduced=9', 'introduced-x86=21'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api( + ['introduced=21', 'introduced-arm=9'], 'arm', 14)) + self.assertTrue(gsl.symbol_in_api( + ['future'], 'arm', gsl.FUTURE_API_LEVEL)) + + self.assertFalse(gsl.symbol_in_api(['introduced=14'], 'arm', 9)) + self.assertFalse(gsl.symbol_in_api(['introduced-arm=14'], 'arm', 9)) + self.assertFalse(gsl.symbol_in_api(['future'], 'arm', 9)) + self.assertFalse(gsl.symbol_in_api( + ['introduced=9', 'future'], 'arm', 14)) + self.assertFalse(gsl.symbol_in_api( + ['introduced-arm=9', 'future'], 'arm', 14)) + self.assertFalse(gsl.symbol_in_api( + ['introduced-arm=21', 'introduced-x86=9'], 'arm', 14)) + self.assertFalse(gsl.symbol_in_api( + ['introduced=9', 'introduced-arm=21'], 'arm', 14)) + self.assertFalse(gsl.symbol_in_api( + ['introduced=21', 'introduced-x86=9'], 'arm', 14)) + + # Interesting edge case: this symbol should be omitted from the + # library, but this call should still return true because none of the + # tags indiciate that it's not present in this API level. + self.assertTrue(gsl.symbol_in_api(['x86'], 'arm', 9)) + + def test_verioned_in_api(self): + self.assertTrue(gsl.symbol_versioned_in_api([], 9)) + self.assertTrue(gsl.symbol_versioned_in_api(['versioned=9'], 9)) + self.assertTrue(gsl.symbol_versioned_in_api(['versioned=9'], 14)) + + self.assertFalse(gsl.symbol_versioned_in_api(['versioned=14'], 9)) + + +class OmitVersionTest(unittest.TestCase): + def test_omit_private(self): + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, [], []), 'arm', 9, False, False)) + + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo_PRIVATE', None, [], []), 'arm', 9, False, False)) + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo_PLATFORM', None, [], []), 'arm', 9, False, False)) + + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo', None, ['platform-only'], []), 'arm', 9, + False, False)) + + def test_omit_llndk(self): + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo', None, ['llndk'], []), 'arm', 9, False, False)) + + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, [], []), 'arm', 9, True, False)) + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, ['llndk'], []), 'arm', 9, True, False)) + + def test_omit_apex(self): + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo', None, ['apex'], []), 'arm', 9, False, False)) + + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, [], []), 'arm', 9, False, True)) + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, ['apex'], []), 'arm', 9, False, True)) + + def test_omit_arch(self): + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, [], []), 'arm', 9, False, False)) + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, ['arm'], []), 'arm', 9, False, False)) + + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo', None, ['x86'], []), 'arm', 9, False, False)) + + def test_omit_api(self): + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, [], []), 'arm', 9, False, False)) + self.assertFalse( + gsl.should_omit_version( + gsl.Version('foo', None, ['introduced=9'], []), 'arm', 9, + False, False)) + + self.assertTrue( + gsl.should_omit_version( + gsl.Version('foo', None, ['introduced=14'], []), 'arm', 9, + False, False)) + + +class OmitSymbolTest(unittest.TestCase): + def test_omit_llndk(self): + self.assertTrue( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['llndk']), 'arm', 9, False, False)) + + self.assertFalse( + gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, True, False)) + self.assertFalse( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['llndk']), 'arm', 9, True, False)) + + def test_omit_apex(self): + self.assertTrue( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['apex']), 'arm', 9, False, False)) + + self.assertFalse( + gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, True)) + self.assertFalse( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['apex']), 'arm', 9, False, True)) + + def test_omit_arch(self): + self.assertFalse( + gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, False)) + self.assertFalse( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['arm']), 'arm', 9, False, False)) + + self.assertTrue( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['x86']), 'arm', 9, False, False)) + + def test_omit_api(self): + self.assertFalse( + gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, False)) + self.assertFalse( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['introduced=9']), 'arm', 9, False, False)) + + self.assertTrue( + gsl.should_omit_symbol( + gsl.Symbol('foo', ['introduced=14']), 'arm', 9, False, False)) + + +class SymbolFileParseTest(unittest.TestCase): + def test_next_line(self): + input_file = io.StringIO(textwrap.dedent("""\ + foo + + bar + # baz + qux + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + self.assertIsNone(parser.current_line) + + self.assertEqual('foo', parser.next_line().strip()) + self.assertEqual('foo', parser.current_line.strip()) + + self.assertEqual('bar', parser.next_line().strip()) + self.assertEqual('bar', parser.current_line.strip()) + + self.assertEqual('qux', parser.next_line().strip()) + self.assertEqual('qux', parser.current_line.strip()) + + self.assertEqual('', parser.next_line()) + self.assertEqual('', parser.current_line) + + def test_parse_version(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { # foo bar + baz; + qux; # woodly doodly + }; + + VERSION_2 { + } VERSION_1; # asdf + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + + parser.next_line() + version = parser.parse_version() + self.assertEqual('VERSION_1', version.name) + self.assertIsNone(version.base) + self.assertEqual(['foo', 'bar'], version.tags) + + expected_symbols = [ + gsl.Symbol('baz', []), + gsl.Symbol('qux', ['woodly', 'doodly']), + ] + self.assertEqual(expected_symbols, version.symbols) + + parser.next_line() + version = parser.parse_version() + self.assertEqual('VERSION_2', version.name) + self.assertEqual('VERSION_1', version.base) + self.assertEqual([], version.tags) + + def test_parse_version_eof(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.next_line() + with self.assertRaises(gsl.ParseError): + parser.parse_version() + + def test_unknown_scope_label(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + foo: + } + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.next_line() + with self.assertRaises(gsl.ParseError): + parser.parse_version() + + def test_parse_symbol(self): + input_file = io.StringIO(textwrap.dedent("""\ + foo; + bar; # baz qux + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + + parser.next_line() + symbol = parser.parse_symbol() + self.assertEqual('foo', symbol.name) + self.assertEqual([], symbol.tags) + + parser.next_line() + symbol = parser.parse_symbol() + self.assertEqual('bar', symbol.name) + self.assertEqual(['baz', 'qux'], symbol.tags) + + def test_wildcard_symbol_global(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + *; + }; + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.next_line() + with self.assertRaises(gsl.ParseError): + parser.parse_version() + + def test_wildcard_symbol_local(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + local: + *; + }; + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.next_line() + version = parser.parse_version() + self.assertEqual([], version.symbols) + + def test_missing_semicolon(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + foo + }; + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.next_line() + with self.assertRaises(gsl.ParseError): + parser.parse_version() + + def test_parse_fails_invalid_input(self): + with self.assertRaises(gsl.ParseError): + input_file = io.StringIO('foo') + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + parser.parse() + + def test_parse(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + local: + hidden1; + global: + foo; + bar; # baz + }; + + VERSION_2 { # wasd + # Implicit global scope. + woodly; + doodly; # asdf + local: + qwerty; + } VERSION_1; + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + versions = parser.parse() + + expected = [ + gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', []), + gsl.Symbol('bar', ['baz']), + ]), + gsl.Version('VERSION_2', 'VERSION_1', ['wasd'], [ + gsl.Symbol('woodly', []), + gsl.Symbol('doodly', ['asdf']), + ]), + ] + + self.assertEqual(expected, versions) + + def test_parse_llndk_apex_symbol(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + foo; + bar; # llndk + baz; # llndk apex + qux; # apex + }; + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, True) + + parser.next_line() + version = parser.parse_version() + self.assertEqual('VERSION_1', version.name) + self.assertIsNone(version.base) + + expected_symbols = [ + gsl.Symbol('foo', []), + gsl.Symbol('bar', ['llndk']), + gsl.Symbol('baz', ['llndk', 'apex']), + gsl.Symbol('qux', ['apex']), + ] + self.assertEqual(expected_symbols, version.symbols) + + +class GeneratorTest(unittest.TestCase): + def test_omit_version(self): + # Thorough testing of the cases involved here is handled by + # OmitVersionTest, PrivateVersionTest, and SymbolPresenceTest. + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False) + + version = gsl.Version('VERSION_PRIVATE', None, [], [ + gsl.Symbol('foo', []), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + version = gsl.Version('VERSION', None, ['x86'], [ + gsl.Symbol('foo', []), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + version = gsl.Version('VERSION', None, ['introduced=14'], [ + gsl.Symbol('foo', []), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + def test_omit_symbol(self): + # Thorough testing of the cases involved here is handled by + # SymbolPresenceTest. + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False) + + version = gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', ['x86']), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + version = gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', ['introduced=14']), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + version = gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', ['llndk']), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + version = gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', ['apex']), + ]) + generator.write_version(version) + self.assertEqual('', src_file.getvalue()) + self.assertEqual('', version_file.getvalue()) + + def test_write(self): + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False) + + versions = [ + gsl.Version('VERSION_1', None, [], [ + gsl.Symbol('foo', []), + gsl.Symbol('bar', ['var']), + gsl.Symbol('woodly', ['weak']), + gsl.Symbol('doodly', ['weak', 'var']), + ]), + gsl.Version('VERSION_2', 'VERSION_1', [], [ + gsl.Symbol('baz', []), + ]), + gsl.Version('VERSION_3', 'VERSION_1', [], [ + gsl.Symbol('qux', ['versioned=14']), + ]), + ] + + generator.write(versions) + expected_src = textwrap.dedent("""\ + void foo() {} + int bar = 0; + __attribute__((weak)) void woodly() {} + __attribute__((weak)) int doodly = 0; + void baz() {} + void qux() {} + """) + self.assertEqual(expected_src, src_file.getvalue()) + + expected_version = textwrap.dedent("""\ + VERSION_1 { + global: + foo; + bar; + woodly; + doodly; + }; + VERSION_2 { + global: + baz; + } VERSION_1; + """) + self.assertEqual(expected_version, version_file.getvalue()) + + +class IntegrationTest(unittest.TestCase): + def test_integration(self): + api_map = { + 'O': 9000, + 'P': 9001, + } + + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + global: + foo; # var + bar; # x86 + fizz; # introduced=O + buzz; # introduced=P + local: + *; + }; + + VERSION_2 { # arm + baz; # introduced=9 + qux; # versioned=14 + } VERSION_1; + + VERSION_3 { # introduced=14 + woodly; + doodly; # var + } VERSION_2; + + VERSION_4 { # versioned=9 + wibble; + wizzes; # llndk + waggle; # apex + } VERSION_2; + + VERSION_5 { # versioned=14 + wobble; + } VERSION_4; + """)) + parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9, False, False) + versions = parser.parse() + + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False) + generator.write(versions) + + expected_src = textwrap.dedent("""\ + int foo = 0; + void baz() {} + void qux() {} + void wibble() {} + void wobble() {} + """) + self.assertEqual(expected_src, src_file.getvalue()) + + expected_version = textwrap.dedent("""\ + VERSION_1 { + global: + foo; + }; + VERSION_2 { + global: + baz; + } VERSION_1; + VERSION_4 { + global: + wibble; + } VERSION_2; + """) + self.assertEqual(expected_version, version_file.getvalue()) + + def test_integration_future_api(self): + api_map = { + 'O': 9000, + 'P': 9001, + 'Q': 9002, + } + + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + global: + foo; # introduced=O + bar; # introduced=P + baz; # introduced=Q + local: + *; + }; + """)) + parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9001, False, False) + versions = parser.parse() + + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9001, False, False) + generator.write(versions) + + expected_src = textwrap.dedent("""\ + void foo() {} + void bar() {} + """) + self.assertEqual(expected_src, src_file.getvalue()) + + expected_version = textwrap.dedent("""\ + VERSION_1 { + global: + foo; + bar; + }; + """) + self.assertEqual(expected_version, version_file.getvalue()) + + def test_multiple_definition(self): + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + global: + foo; + foo; + bar; + baz; + qux; # arm + local: + *; + }; + + VERSION_2 { + global: + bar; + qux; # arm64 + } VERSION_1; + + VERSION_PRIVATE { + global: + baz; + } VERSION_2; + + """)) + parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False) + + with self.assertRaises(gsl.MultiplyDefinedSymbolError) as cm: + parser.parse() + self.assertEquals(['bar', 'foo'], + cm.exception.multiply_defined_symbols) + + def test_integration_with_apex(self): + api_map = { + 'O': 9000, + 'P': 9001, + } + + input_file = io.StringIO(textwrap.dedent("""\ + VERSION_1 { + global: + foo; # var + bar; # x86 + fizz; # introduced=O + buzz; # introduced=P + local: + *; + }; + + VERSION_2 { # arm + baz; # introduced=9 + qux; # versioned=14 + } VERSION_1; + + VERSION_3 { # introduced=14 + woodly; + doodly; # var + } VERSION_2; + + VERSION_4 { # versioned=9 + wibble; + wizzes; # llndk + waggle; # apex + bubble; # apex llndk + duddle; # llndk apex + } VERSION_2; + + VERSION_5 { # versioned=14 + wobble; + } VERSION_4; + """)) + parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9, False, True) + versions = parser.parse() + + src_file = io.StringIO() + version_file = io.StringIO() + generator = gsl.Generator(src_file, version_file, 'arm', 9, False, True) + generator.write(versions) + + expected_src = textwrap.dedent("""\ + int foo = 0; + void baz() {} + void qux() {} + void wibble() {} + void waggle() {} + void bubble() {} + void duddle() {} + void wobble() {} + """) + self.assertEqual(expected_src, src_file.getvalue()) + + expected_version = textwrap.dedent("""\ + VERSION_1 { + global: + foo; + }; + VERSION_2 { + global: + baz; + } VERSION_1; + VERSION_4 { + global: + wibble; + waggle; + bubble; + duddle; + } VERSION_2; + """) + self.assertEqual(expected_version, version_file.getvalue()) + +def main(): + suite = unittest.TestLoader().loadTestsFromName(__name__) + unittest.TextTestRunner(verbosity=3).run(suite) + + +if __name__ == '__main__': + main() diff --git a/cc/scriptlib/test_ndk_api_coverage_parser.py b/cc/scriptlib/test_ndk_api_coverage_parser.py new file mode 100644 index 000000000..a3c9b708c --- /dev/null +++ b/cc/scriptlib/test_ndk_api_coverage_parser.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""Tests for ndk_api_coverage_parser.py.""" +import io +import textwrap +import unittest + +from xml.etree.ElementTree import tostring +from gen_stub_libs import FUTURE_API_LEVEL, SymbolFileParser +import ndk_api_coverage_parser as nparser + + +# pylint: disable=missing-docstring + + +class ApiCoverageSymbolFileParserTest(unittest.TestCase): + def test_parse(self): + input_file = io.StringIO(textwrap.dedent(u"""\ + LIBLOG { # introduced-arm64=24 introduced-x86=24 introduced-x86_64=24 + global: + android_name_to_log_id; # apex llndk introduced=23 + android_log_id_to_name; # llndk arm + __android_log_assert; # introduced-x86=23 + __android_log_buf_print; # var + __android_log_buf_write; + local: + *; + }; + + LIBLOG_PLATFORM { + android_fdtrack; # llndk + android_net; # introduced=23 + }; + + LIBLOG_FOO { # var + android_var; + }; + """)) + parser = SymbolFileParser(input_file, {}, "", FUTURE_API_LEVEL, True, True) + generator = nparser.XmlGenerator(io.StringIO()) + result = tostring(generator.convertToXml(parser.parse())).decode() + expected = '<ndk-library><symbol apex="True" arch="" introduced="23" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_name_to_log_id" /><symbol arch="arm" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_log_id_to_name" /><symbol arch="" introduced-arm64="24" introduced-x86="23" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_assert" /><symbol arch="" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_buf_write" /><symbol arch="" is_deprecated="False" is_platform="True" llndk="True" name="android_fdtrack" /><symbol arch="" introduced="23" is_deprecated="False" is_platform="True" name="android_net" /></ndk-library>' + self.assertEqual(expected, result) + + +def main(): + suite = unittest.TestLoader().loadTestsFromName(__name__) + unittest.TextTestRunner(verbosity=3).run(suite) + + +if __name__ == '__main__': + main() |