Add support for named versions in NDK map files.

Test: nose2
Test: make checkbuild
Bug: None
Change-Id: Ic32cfb3e0db767f695b617c787733a6ef75030aa
diff --git a/cc/gen_stub_libs.py b/cc/gen_stub_libs.py
index 8a4f21e..bed718c 100755
--- a/cc/gen_stub_libs.py
+++ b/cc/gen_stub_libs.py
@@ -41,27 +41,56 @@
     return logging.getLogger(__name__)
 
 
-def api_level_arg(api_str):
-    """Parses an API level, handling the "current" special case.
-
-    Args:
-        api_str: (string) Either a numeric API level or "current".
-
-    Returns:
-        (int) FUTURE_API_LEVEL if `api_str` is "current", else `api_str` parsed
-        as an integer.
-    """
-    if api_str == "current":
-        return FUTURE_API_LEVEL
-    return int(api_str)
-
-
 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.
 
@@ -70,9 +99,7 @@
 
     Returns: Value part of tag as a string.
     """
-    if '=' not in tag:
-        raise ValueError('Not a key/value tag: ' + tag)
-    return tag.partition('=')[2]
+    return split_tag(tag)[1]
 
 
 def version_is_private(version):
@@ -194,8 +221,9 @@
 
 class SymbolFileParser(object):
     """Parses NDK symbol files."""
-    def __init__(self, input_file):
+    def __init__(self, input_file, api_map):
         self.input_file = input_file
+        self.api_map = api_map
         self.current_line = None
 
     def parse(self):
@@ -213,6 +241,7 @@
         """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
         while self.next_line() != '':
@@ -254,6 +283,7 @@
         # 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):
@@ -342,10 +372,7 @@
     if api == "current":
         return FUTURE_API_LEVEL
 
-    with open(api_map) as map_file:
-        api_levels = json.load(map_file)
-
-    return api_levels[api]
+    return api_map[api]
 
 
 def parse_args():
@@ -382,7 +409,9 @@
     """Program entry point."""
     args = parse_args()
 
-    api = decode_api_level(args.api, args.api_map)
+    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
@@ -391,7 +420,7 @@
     logging.basicConfig(level=verbose_map[verbosity])
 
     with open(args.symbol_file) as symbol_file:
-        versions = SymbolFileParser(symbol_file).parse()
+        versions = SymbolFileParser(symbol_file, api_map).parse()
 
     with open(args.stub_src, 'w') as src_file:
         with open(args.version_script, 'w') as version_file:
diff --git a/cc/test_gen_stub_libs.py b/cc/test_gen_stub_libs.py
index 8611ef3..4df6cf8 100755
--- a/cc/test_gen_stub_libs.py
+++ b/cc/test_gen_stub_libs.py
@@ -25,6 +25,15 @@
 # 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(''))
@@ -34,12 +43,59 @@
         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):
@@ -151,7 +207,7 @@
             # baz
             qux
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         self.assertIsNone(parser.current_line)
 
         self.assertEqual('foo', parser.next_line().strip())
@@ -176,7 +232,7 @@
             VERSION_2 {
             } VERSION_1; # asdf
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
 
         parser.next_line()
         version = parser.parse_version()
@@ -200,7 +256,7 @@
         input_file = cStringIO.StringIO(textwrap.dedent("""\
             VERSION_1 {
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         parser.next_line()
         with self.assertRaises(gsl.ParseError):
             parser.parse_version()
@@ -211,7 +267,7 @@
                 foo:
             }
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         parser.next_line()
         with self.assertRaises(gsl.ParseError):
             parser.parse_version()
@@ -221,7 +277,7 @@
             foo;
             bar; # baz qux
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
 
         parser.next_line()
         symbol = parser.parse_symbol()
@@ -239,7 +295,7 @@
                 *;
             };
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         parser.next_line()
         with self.assertRaises(gsl.ParseError):
             parser.parse_version()
@@ -251,7 +307,7 @@
                     *;
             };
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         parser.next_line()
         version = parser.parse_version()
         self.assertEqual([], version.symbols)
@@ -262,7 +318,7 @@
                 foo
             };
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         parser.next_line()
         with self.assertRaises(gsl.ParseError):
             parser.parse_version()
@@ -270,7 +326,7 @@
     def test_parse_fails_invalid_input(self):
         with self.assertRaises(gsl.ParseError):
             input_file = cStringIO.StringIO('foo')
-            parser = gsl.SymbolFileParser(input_file)
+            parser = gsl.SymbolFileParser(input_file, {})
             parser.parse()
 
     def test_parse(self):
@@ -291,7 +347,7 @@
                     qwerty;
             } VERSION_1;
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, {})
         versions = parser.parse()
 
         expected = [
@@ -408,11 +464,18 @@
 
 class IntegrationTest(unittest.TestCase):
     def test_integration(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+        }
+
         input_file = cStringIO.StringIO(textwrap.dedent("""\
             VERSION_1 {
                 global:
                     foo; # var
                     bar; # x86
+                    fizz; # introduced=O
+                    buzz; # introduced=P
                 local:
                     *;
             };
@@ -436,7 +499,7 @@
                 wobble;
             } VERSION_4;
         """))
-        parser = gsl.SymbolFileParser(input_file)
+        parser = gsl.SymbolFileParser(input_file, api_map)
         versions = parser.parse()
 
         src_file = cStringIO.StringIO()
@@ -469,6 +532,46 @@
         """)
         self.assertEqual(expected_version, version_file.getvalue())
 
+    def test_integration_future_api(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+            'Q': 9002,
+        }
+
+        input_file = cStringIO.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo; # introduced=O
+                    bar; # introduced=P
+                    baz; # introduced=Q
+                local:
+                    *;
+            };
+        """))
+        parser = gsl.SymbolFileParser(input_file, api_map)
+        versions = parser.parse()
+
+        src_file = cStringIO.StringIO()
+        version_file = cStringIO.StringIO()
+        generator = gsl.Generator(src_file, version_file, 'arm', 9001, 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 main():
     suite = unittest.TestLoader().loadTestsFromName(__name__)