platform_build_soong/cc/symbolfile/test_symbolfile.py
Jooyung Han 33eb615eb0 ndkstubgen: use llndk=<version> for new llndk stub
We want LLNDK symbols to be explicitly marked with llndk tag to
handle LLNDK freezing which happens before SDK freezing. If symbols
need to be frozen as LLNDK, those symbols must be marked explicitly with
correct vFRC version.

In the following example,

LIBFOO { # introduced=35
  foo;
  bar;
  bar; # llndk=202404
  baz; # llndk=202404
  qux; # llndk=202505
};

NDK libfoo will have foo and bar while LLNDK libfoo stub will have bar
and baz for 202404.

Bug: 329012338
Test: test_ndkstubgen test_symbolfile
Change-Id: I384f589b240fa047e8871964bf9550f426024dfc
2024-03-14 06:06:26 +09:00

666 lines
23 KiB
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 symbolfile."""
import io
import textwrap
import unittest
import symbolfile
from symbolfile import Arch, Tag, Tags, Version, Symbol, Filter
from copy import copy
# pylint: disable=missing-docstring
class DecodeApiLevelTest(unittest.TestCase):
def test_decode_api_level(self) -> None:
self.assertEqual(9, symbolfile.decode_api_level('9', {}))
self.assertEqual(9000, symbolfile.decode_api_level('O', {'O': 9000}))
with self.assertRaises(KeyError):
symbolfile.decode_api_level('O', {})
class TagsTest(unittest.TestCase):
def test_get_tags_no_tags(self) -> None:
self.assertEqual(Tags(), symbolfile.get_tags('', {}))
self.assertEqual(Tags(), symbolfile.get_tags('foo bar baz', {}))
def test_get_tags(self) -> None:
self.assertEqual(Tags.from_strs(['llndk', 'apex']),
symbolfile.get_tags('# llndk apex', {}))
self.assertEqual(Tags.from_strs(['llndk', 'apex']),
symbolfile.get_tags('foo # llndk apex', {}))
def test_get_unrecognized_tags(self) -> None:
with self.assertRaises(symbolfile.ParseError):
symbolfile.get_tags('# bar', {})
with self.assertRaises(symbolfile.ParseError):
symbolfile.get_tags('foo # bar', {})
with self.assertRaises(symbolfile.ParseError):
symbolfile.get_tags('# #', {})
with self.assertRaises(symbolfile.ParseError):
symbolfile.get_tags('# apex # llndk', {})
def test_split_tag(self) -> None:
self.assertTupleEqual(('foo', 'bar'),
symbolfile.split_tag(Tag('foo=bar')))
self.assertTupleEqual(('foo', 'bar=baz'),
symbolfile.split_tag(Tag('foo=bar=baz')))
with self.assertRaises(ValueError):
symbolfile.split_tag(Tag('foo'))
def test_get_tag_value(self) -> None:
self.assertEqual('bar', symbolfile.get_tag_value(Tag('foo=bar')))
self.assertEqual('bar=baz',
symbolfile.get_tag_value(Tag('foo=bar=baz')))
with self.assertRaises(ValueError):
symbolfile.get_tag_value(Tag('foo'))
def test_is_api_level_tag(self) -> None:
self.assertTrue(symbolfile.is_api_level_tag(Tag('introduced=24')))
self.assertTrue(symbolfile.is_api_level_tag(Tag('introduced-arm=24')))
self.assertTrue(symbolfile.is_api_level_tag(Tag('versioned=24')))
# Shouldn't try to process things that aren't a key/value tag.
self.assertFalse(symbolfile.is_api_level_tag(Tag('arm')))
self.assertFalse(symbolfile.is_api_level_tag(Tag('introduced')))
self.assertFalse(symbolfile.is_api_level_tag(Tag('versioned')))
# We don't support arch specific `versioned` tags.
self.assertFalse(symbolfile.is_api_level_tag(Tag('versioned-arm=24')))
def test_decode_api_level_tags(self) -> None:
api_map = {
'O': 9000,
'P': 9001,
}
tags = [
symbolfile.decode_api_level_tag(t, api_map) for t in (
Tag('introduced=9'),
Tag('introduced-arm=14'),
Tag('versioned=16'),
Tag('arm'),
Tag('introduced=O'),
Tag('introduced=P'),
)
]
expected_tags = [
Tag('introduced=9'),
Tag('introduced-arm=14'),
Tag('versioned=16'),
Tag('arm'),
Tag('introduced=9000'),
Tag('introduced=9001'),
]
self.assertListEqual(expected_tags, tags)
with self.assertRaises(symbolfile.ParseError):
symbolfile.decode_api_level_tag(Tag('introduced=O'), {})
class PrivateVersionTest(unittest.TestCase):
def test_version_is_private(self) -> None:
def mock_version(name: str) -> Version:
return Version(name, base=None, tags=Tags(), symbols=[])
self.assertFalse(mock_version('foo').is_private)
self.assertFalse(mock_version('PRIVATE').is_private)
self.assertFalse(mock_version('PLATFORM').is_private)
self.assertFalse(mock_version('foo_private').is_private)
self.assertFalse(mock_version('foo_platform').is_private)
self.assertFalse(mock_version('foo_PRIVATE_').is_private)
self.assertFalse(mock_version('foo_PLATFORM_').is_private)
self.assertTrue(mock_version('foo_PRIVATE').is_private)
self.assertTrue(mock_version('foo_PLATFORM').is_private)
class SymbolPresenceTest(unittest.TestCase):
def test_symbol_in_arch(self) -> None:
self.assertTrue(symbolfile.symbol_in_arch(Tags(), Arch('arm')))
self.assertTrue(
symbolfile.symbol_in_arch(Tags.from_strs(['arm']), Arch('arm')))
self.assertFalse(
symbolfile.symbol_in_arch(Tags.from_strs(['x86']), Arch('arm')))
def test_symbol_in_api(self) -> None:
self.assertTrue(symbolfile.symbol_in_api([], Arch('arm'), 9))
self.assertTrue(
symbolfile.symbol_in_api([Tag('introduced=9')], Arch('arm'), 9))
self.assertTrue(
symbolfile.symbol_in_api([Tag('introduced=9')], Arch('arm'), 14))
self.assertTrue(
symbolfile.symbol_in_api([Tag('introduced-arm=9')], Arch('arm'),
14))
self.assertTrue(
symbolfile.symbol_in_api([Tag('introduced-arm=9')], Arch('arm'),
14))
self.assertTrue(
symbolfile.symbol_in_api([Tag('introduced-x86=14')], Arch('arm'),
9))
self.assertTrue(
symbolfile.symbol_in_api(
[Tag('introduced-arm=9'),
Tag('introduced-x86=21')], Arch('arm'), 14))
self.assertTrue(
symbolfile.symbol_in_api(
[Tag('introduced=9'),
Tag('introduced-x86=21')], Arch('arm'), 14))
self.assertTrue(
symbolfile.symbol_in_api(
[Tag('introduced=21'),
Tag('introduced-arm=9')], Arch('arm'), 14))
self.assertTrue(
symbolfile.symbol_in_api([Tag('future')], Arch('arm'),
symbolfile.FUTURE_API_LEVEL))
self.assertFalse(
symbolfile.symbol_in_api([Tag('introduced=14')], Arch('arm'), 9))
self.assertFalse(
symbolfile.symbol_in_api([Tag('introduced-arm=14')], Arch('arm'),
9))
self.assertFalse(
symbolfile.symbol_in_api([Tag('future')], Arch('arm'), 9))
self.assertFalse(
symbolfile.symbol_in_api(
[Tag('introduced=9'), Tag('future')], Arch('arm'), 14))
self.assertFalse(
symbolfile.symbol_in_api([Tag('introduced-arm=9'),
Tag('future')], Arch('arm'), 14))
self.assertFalse(
symbolfile.symbol_in_api(
[Tag('introduced-arm=21'),
Tag('introduced-x86=9')], Arch('arm'), 14))
self.assertFalse(
symbolfile.symbol_in_api(
[Tag('introduced=9'),
Tag('introduced-arm=21')], Arch('arm'), 14))
self.assertFalse(
symbolfile.symbol_in_api(
[Tag('introduced=21'),
Tag('introduced-x86=9')], Arch('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(symbolfile.symbol_in_api([Tag('x86')], Arch('arm'), 9))
def test_verioned_in_api(self) -> None:
self.assertTrue(symbolfile.symbol_versioned_in_api([], 9))
self.assertTrue(
symbolfile.symbol_versioned_in_api([Tag('versioned=9')], 9))
self.assertTrue(
symbolfile.symbol_versioned_in_api([Tag('versioned=9')], 14))
self.assertFalse(
symbolfile.symbol_versioned_in_api([Tag('versioned=14')], 9))
class OmitVersionTest(unittest.TestCase):
def setUp(self) -> None:
self.filter = Filter(arch = Arch('arm'), api = 9)
self.version = Version('foo', None, Tags(), [])
def assertOmit(self, f: Filter, v: Version) -> None:
self.assertTrue(f.should_omit_version(v))
def assertInclude(self, f: Filter, v: Version) -> None:
self.assertFalse(f.should_omit_version(v))
def test_omit_private(self) -> None:
f = self.filter
v = self.version
self.assertInclude(f, v)
v.name = 'foo_PRIVATE'
self.assertOmit(f, v)
v.name = 'foo_PLATFORM'
self.assertOmit(f, v)
v.name = 'foo'
v.tags = Tags.from_strs(['platform-only'])
self.assertOmit(f, v)
def test_omit_llndk(self) -> None:
f = self.filter
v = self.version
v_llndk = copy(v)
v_llndk.tags = Tags.from_strs(['llndk'])
self.assertOmit(f, v_llndk)
f.llndk = True
self.assertInclude(f, v)
self.assertInclude(f, v_llndk)
def test_omit_apex(self) -> None:
f = self.filter
v = self.version
v_apex = copy(v)
v_apex.tags = Tags.from_strs(['apex'])
v_systemapi = copy(v)
v_systemapi.tags = Tags.from_strs(['systemapi'])
self.assertOmit(f, v_apex)
f.apex = True
self.assertInclude(f, v)
self.assertInclude(f, v_apex)
self.assertOmit(f, v_systemapi)
def test_omit_systemapi(self) -> None:
f = self.filter
v = self.version
v_apex = copy(v)
v_apex.tags = Tags.from_strs(['apex'])
v_systemapi = copy(v)
v_systemapi.tags = Tags.from_strs(['systemapi'])
self.assertOmit(f, v_systemapi)
f.systemapi = True
self.assertInclude(f, v)
self.assertInclude(f, v_systemapi)
self.assertOmit(f, v_apex)
def test_omit_arch(self) -> None:
f_arm = self.filter
v_none = self.version
self.assertInclude(f_arm, v_none)
v_arm = copy(v_none)
v_arm.tags = Tags.from_strs(['arm'])
self.assertInclude(f_arm, v_arm)
v_x86 = copy(v_none)
v_x86.tags = Tags.from_strs(['x86'])
self.assertOmit(f_arm, v_x86)
def test_omit_api(self) -> None:
f_api9 = self.filter
v_none = self.version
self.assertInclude(f_api9, v_none)
v_api9 = copy(v_none)
v_api9.tags = Tags.from_strs(['introduced=9'])
self.assertInclude(f_api9, v_api9)
v_api14 = copy(v_none)
v_api14.tags = Tags.from_strs(['introduced=14'])
self.assertOmit(f_api9, v_api14)
class OmitSymbolTest(unittest.TestCase):
def setUp(self) -> None:
self.filter = Filter(arch = Arch('arm'), api = 9)
def assertOmit(self, f: Filter, s: Symbol) -> None:
self.assertTrue(f.should_omit_symbol(s))
def assertInclude(self, f: Filter, s: Symbol) -> None:
self.assertFalse(f.should_omit_symbol(s))
def test_omit_ndk(self) -> None:
f_ndk = self.filter
f_nondk = copy(f_ndk)
f_nondk.ndk = False
f_nondk.apex = True
s_ndk = Symbol('foo', Tags())
s_nonndk = Symbol('foo', Tags.from_strs(['apex']))
self.assertInclude(f_ndk, s_ndk)
self.assertOmit(f_ndk, s_nonndk)
self.assertOmit(f_nondk, s_ndk)
self.assertInclude(f_nondk, s_nonndk)
def test_omit_llndk(self) -> None:
f_none = self.filter
f_llndk = copy(f_none)
f_llndk.llndk = True
s_none = Symbol('foo', Tags())
s_llndk = Symbol('foo', Tags.from_strs(['llndk']))
self.assertOmit(f_none, s_llndk)
self.assertInclude(f_llndk, s_none)
self.assertInclude(f_llndk, s_llndk)
def test_omit_llndk_versioned(self) -> None:
f_ndk = self.filter
f_ndk.api = 35
f_llndk = copy(f_ndk)
f_llndk.llndk = True
f_llndk.api = 202404
s = Symbol('foo', Tags())
s_llndk = Symbol('foo', Tags.from_strs(['llndk']))
s_llndk_202404 = Symbol('foo', Tags.from_strs(['llndk=202404']))
s_34 = Symbol('foo', Tags.from_strs(['introduced=34']))
s_34_llndk = Symbol('foo', Tags.from_strs(['introduced=34', 'llndk']))
s_35 = Symbol('foo', Tags.from_strs(['introduced=35']))
s_35_llndk_202404 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202404']))
s_35_llndk_202504 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202504']))
# When targeting NDK, omit LLNDK tags
self.assertInclude(f_ndk, s)
self.assertOmit(f_ndk, s_llndk)
self.assertOmit(f_ndk, s_llndk_202404)
self.assertInclude(f_ndk, s_34)
self.assertOmit(f_ndk, s_34_llndk)
self.assertInclude(f_ndk, s_35)
self.assertOmit(f_ndk, s_35_llndk_202404)
self.assertOmit(f_ndk, s_35_llndk_202504)
# When targeting LLNDK, old symbols without any mode tags are included as LLNDK
self.assertInclude(f_llndk, s)
# When targeting LLNDK, old symbols with #llndk are included as LLNDK
self.assertInclude(f_llndk, s_llndk)
self.assertInclude(f_llndk, s_llndk_202404)
self.assertInclude(f_llndk, s_34)
self.assertInclude(f_llndk, s_34_llndk)
# When targeting LLNDK, new symbols(>=35) should be tagged with llndk-introduced=.
self.assertOmit(f_llndk, s_35)
self.assertInclude(f_llndk, s_35_llndk_202404)
self.assertOmit(f_llndk, s_35_llndk_202504)
def test_omit_apex(self) -> None:
f_none = self.filter
f_apex = copy(f_none)
f_apex.apex = True
s_none = Symbol('foo', Tags())
s_apex = Symbol('foo', Tags.from_strs(['apex']))
s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
self.assertOmit(f_none, s_apex)
self.assertInclude(f_apex, s_none)
self.assertInclude(f_apex, s_apex)
self.assertOmit(f_apex, s_systemapi)
def test_omit_systemapi(self) -> None:
f_none = self.filter
f_systemapi = copy(f_none)
f_systemapi.systemapi = True
s_none = Symbol('foo', Tags())
s_apex = Symbol('foo', Tags.from_strs(['apex']))
s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
self.assertOmit(f_none, s_systemapi)
self.assertInclude(f_systemapi, s_none)
self.assertInclude(f_systemapi, s_systemapi)
self.assertOmit(f_systemapi, s_apex)
def test_omit_apex_and_systemapi(self) -> None:
f = self.filter
f.systemapi = True
f.apex = True
s_none = Symbol('foo', Tags())
s_apex = Symbol('foo', Tags.from_strs(['apex']))
s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
self.assertInclude(f, s_none)
self.assertInclude(f, s_apex)
self.assertInclude(f, s_systemapi)
def test_omit_arch(self) -> None:
f_arm = self.filter
s_none = Symbol('foo', Tags())
s_arm = Symbol('foo', Tags.from_strs(['arm']))
s_x86 = Symbol('foo', Tags.from_strs(['x86']))
self.assertInclude(f_arm, s_none)
self.assertInclude(f_arm, s_arm)
self.assertOmit(f_arm, s_x86)
def test_omit_api(self) -> None:
f_api9 = self.filter
s_none = Symbol('foo', Tags())
s_api9 = Symbol('foo', Tags.from_strs(['introduced=9']))
s_api14 = Symbol('foo', Tags.from_strs(['introduced=14']))
self.assertInclude(f_api9, s_none)
self.assertInclude(f_api9, s_api9)
self.assertOmit(f_api9, s_api14)
class SymbolFileParseTest(unittest.TestCase):
def setUp(self) -> None:
self.filter = Filter(arch = Arch('arm'), api = 16)
def test_next_line(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
foo
bar
# baz
qux
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
self.assertIsNone(parser.current_line)
self.assertEqual('foo', parser.next_line().strip())
assert parser.current_line is not None
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) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 { # weak introduced=35
baz;
qux; # apex llndk
};
VERSION_2 {
} VERSION_1; # not-a-tag
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
version = parser.parse_version()
self.assertEqual('VERSION_1', version.name)
self.assertIsNone(version.base)
self.assertEqual(Tags.from_strs(['weak', 'introduced=35']), version.tags)
# Inherit introduced= tags from version block so that
# should_omit_tags() can differently based on introduced API level when treating
# LLNDK-available symbols.
expected_symbols = [
Symbol('baz', Tags.from_strs(['introduced=35'])),
Symbol('qux', Tags.from_strs(['apex', 'llndk', 'introduced=35'])),
]
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(Tags(), version.tags)
def test_parse_version_eof(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
with self.assertRaises(symbolfile.ParseError):
parser.parse_version()
def test_unknown_scope_label(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
foo:
}
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
with self.assertRaises(symbolfile.ParseError):
parser.parse_version()
def test_parse_symbol(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
foo;
bar; # llndk apex
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
symbol = parser.parse_symbol()
self.assertEqual('foo', symbol.name)
self.assertEqual(Tags(), symbol.tags)
parser.next_line()
symbol = parser.parse_symbol()
self.assertEqual('bar', symbol.name)
self.assertEqual(Tags.from_strs(['llndk', 'apex']), symbol.tags)
def test_wildcard_symbol_global(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
*;
};
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
with self.assertRaises(symbolfile.ParseError):
parser.parse_version()
def test_wildcard_symbol_local(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
local:
*;
};
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
version = parser.parse_version()
self.assertEqual([], version.symbols)
def test_missing_semicolon(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
foo
};
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.next_line()
with self.assertRaises(symbolfile.ParseError):
parser.parse_version()
def test_parse_fails_invalid_input(self) -> None:
with self.assertRaises(symbolfile.ParseError):
input_file = io.StringIO('foo')
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
parser.parse()
def test_parse(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
local:
hidden1;
global:
foo;
bar; # llndk
};
VERSION_2 { # weak
# Implicit global scope.
woodly;
doodly; # llndk
local:
qwerty;
} VERSION_1;
"""))
parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
versions = parser.parse()
expected = [
symbolfile.Version('VERSION_1', None, Tags(), [
Symbol('foo', Tags()),
Symbol('bar', Tags.from_strs(['llndk'])),
]),
symbolfile.Version(
'VERSION_2', 'VERSION_1', Tags.from_strs(['weak']), [
Symbol('woodly', Tags()),
Symbol('doodly', Tags.from_strs(['llndk'])),
]),
]
self.assertEqual(expected, versions)
def test_parse_llndk_apex_symbol(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 {
foo;
bar; # llndk
baz; # llndk apex
qux; # apex
};
"""))
f = copy(self.filter)
f.llndk = True
parser = symbolfile.SymbolFileParser(input_file, {}, f)
parser.next_line()
version = parser.parse_version()
self.assertEqual('VERSION_1', version.name)
self.assertIsNone(version.base)
expected_symbols = [
Symbol('foo', Tags()),
Symbol('bar', Tags.from_strs(['llndk'])),
Symbol('baz', Tags.from_strs(['llndk', 'apex'])),
Symbol('qux', Tags.from_strs(['apex'])),
]
self.assertEqual(expected_symbols, version.symbols)
def test_parse_llndk_version_is_missing(self) -> None:
input_file = io.StringIO(textwrap.dedent("""\
VERSION_1 { # introduced=35
foo;
bar; # llndk
};
"""))
f = copy(self.filter)
f.llndk = True
parser = symbolfile.SymbolFileParser(input_file, {}, f)
with self.assertRaises(symbolfile.ParseError):
parser.parse()
def main() -> None:
suite = unittest.TestLoader().loadTestsFromName(__name__)
unittest.TextTestRunner(verbosity=3).run(suite)
if __name__ == '__main__':
main()