1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""Tests for symbolfile."""
17import io
18import textwrap
19import unittest
20
21import symbolfile
22from symbolfile import Arch, Tag, Tags, Version, Symbol, Filter
23from copy import copy
24
25# pylint: disable=missing-docstring
26
27
28class DecodeApiLevelTest(unittest.TestCase):
29    def test_decode_api_level(self) -> None:
30        self.assertEqual(9, symbolfile.decode_api_level('9', {}))
31        self.assertEqual(9000, symbolfile.decode_api_level('O', {'O': 9000}))
32
33        with self.assertRaises(KeyError):
34            symbolfile.decode_api_level('O', {})
35
36
37class TagsTest(unittest.TestCase):
38    def test_get_tags_no_tags(self) -> None:
39        self.assertEqual(Tags(), symbolfile.get_tags('', {}))
40        self.assertEqual(Tags(), symbolfile.get_tags('foo bar baz', {}))
41
42    def test_get_tags(self) -> None:
43        self.assertEqual(Tags.from_strs(['llndk', 'apex']),
44                         symbolfile.get_tags('# llndk apex', {}))
45        self.assertEqual(Tags.from_strs(['llndk', 'apex']),
46                         symbolfile.get_tags('foo # llndk apex', {}))
47
48    def test_get_unrecognized_tags(self) -> None:
49        with self.assertRaises(symbolfile.ParseError):
50            symbolfile.get_tags('# bar', {})
51        with self.assertRaises(symbolfile.ParseError):
52            symbolfile.get_tags('foo # bar', {})
53        with self.assertRaises(symbolfile.ParseError):
54            symbolfile.get_tags('# #', {})
55        with self.assertRaises(symbolfile.ParseError):
56            symbolfile.get_tags('# apex # llndk', {})
57
58    def test_split_tag(self) -> None:
59        self.assertTupleEqual(('foo', 'bar'),
60                              symbolfile.split_tag(Tag('foo=bar')))
61        self.assertTupleEqual(('foo', 'bar=baz'),
62                              symbolfile.split_tag(Tag('foo=bar=baz')))
63        with self.assertRaises(ValueError):
64            symbolfile.split_tag(Tag('foo'))
65
66    def test_get_tag_value(self) -> None:
67        self.assertEqual('bar', symbolfile.get_tag_value(Tag('foo=bar')))
68        self.assertEqual('bar=baz',
69                         symbolfile.get_tag_value(Tag('foo=bar=baz')))
70        with self.assertRaises(ValueError):
71            symbolfile.get_tag_value(Tag('foo'))
72
73    def test_is_api_level_tag(self) -> None:
74        self.assertTrue(symbolfile.is_api_level_tag(Tag('introduced=24')))
75        self.assertTrue(symbolfile.is_api_level_tag(Tag('introduced-arm=24')))
76        self.assertTrue(symbolfile.is_api_level_tag(Tag('versioned=24')))
77
78        # Shouldn't try to process things that aren't a key/value tag.
79        self.assertFalse(symbolfile.is_api_level_tag(Tag('arm')))
80        self.assertFalse(symbolfile.is_api_level_tag(Tag('introduced')))
81        self.assertFalse(symbolfile.is_api_level_tag(Tag('versioned')))
82
83        # We don't support arch specific `versioned` tags.
84        self.assertFalse(symbolfile.is_api_level_tag(Tag('versioned-arm=24')))
85
86    def test_decode_api_level_tags(self) -> None:
87        api_map = {
88            'O': 9000,
89            'P': 9001,
90        }
91
92        tags = [
93            symbolfile.decode_api_level_tag(t, api_map) for t in (
94                Tag('introduced=9'),
95                Tag('introduced-arm=14'),
96                Tag('versioned=16'),
97                Tag('arm'),
98                Tag('introduced=O'),
99                Tag('introduced=P'),
100            )
101        ]
102        expected_tags = [
103            Tag('introduced=9'),
104            Tag('introduced-arm=14'),
105            Tag('versioned=16'),
106            Tag('arm'),
107            Tag('introduced=9000'),
108            Tag('introduced=9001'),
109        ]
110        self.assertListEqual(expected_tags, tags)
111
112        with self.assertRaises(symbolfile.ParseError):
113            symbolfile.decode_api_level_tag(Tag('introduced=O'), {})
114
115
116class PrivateVersionTest(unittest.TestCase):
117    def test_version_is_private(self) -> None:
118        def mock_version(name: str) -> Version:
119            return Version(name, base=None, tags=Tags(), symbols=[])
120
121        self.assertFalse(mock_version('foo').is_private)
122        self.assertFalse(mock_version('PRIVATE').is_private)
123        self.assertFalse(mock_version('PLATFORM').is_private)
124        self.assertFalse(mock_version('foo_private').is_private)
125        self.assertFalse(mock_version('foo_platform').is_private)
126        self.assertFalse(mock_version('foo_PRIVATE_').is_private)
127        self.assertFalse(mock_version('foo_PLATFORM_').is_private)
128
129        self.assertTrue(mock_version('foo_PRIVATE').is_private)
130        self.assertTrue(mock_version('foo_PLATFORM').is_private)
131
132
133class SymbolPresenceTest(unittest.TestCase):
134    def test_symbol_in_arch(self) -> None:
135        self.assertTrue(symbolfile.symbol_in_arch(Tags(), Arch('arm')))
136        self.assertTrue(
137            symbolfile.symbol_in_arch(Tags.from_strs(['arm']), Arch('arm')))
138
139        self.assertFalse(
140            symbolfile.symbol_in_arch(Tags.from_strs(['x86']), Arch('arm')))
141
142    def test_symbol_in_api(self) -> None:
143        self.assertTrue(symbolfile.symbol_in_api([], Arch('arm'), 9))
144        self.assertTrue(
145            symbolfile.symbol_in_api([Tag('introduced=9')], Arch('arm'), 9))
146        self.assertTrue(
147            symbolfile.symbol_in_api([Tag('introduced=9')], Arch('arm'), 14))
148        self.assertTrue(
149            symbolfile.symbol_in_api([Tag('introduced-arm=9')], Arch('arm'),
150                                     14))
151        self.assertTrue(
152            symbolfile.symbol_in_api([Tag('introduced-arm=9')], Arch('arm'),
153                                     14))
154        self.assertTrue(
155            symbolfile.symbol_in_api([Tag('introduced-x86=14')], Arch('arm'),
156                                     9))
157        self.assertTrue(
158            symbolfile.symbol_in_api(
159                [Tag('introduced-arm=9'),
160                 Tag('introduced-x86=21')], Arch('arm'), 14))
161        self.assertTrue(
162            symbolfile.symbol_in_api(
163                [Tag('introduced=9'),
164                 Tag('introduced-x86=21')], Arch('arm'), 14))
165        self.assertTrue(
166            symbolfile.symbol_in_api(
167                [Tag('introduced=21'),
168                 Tag('introduced-arm=9')], Arch('arm'), 14))
169        self.assertTrue(
170            symbolfile.symbol_in_api([Tag('future')], Arch('arm'),
171                                     symbolfile.FUTURE_API_LEVEL))
172
173        self.assertFalse(
174            symbolfile.symbol_in_api([Tag('introduced=14')], Arch('arm'), 9))
175        self.assertFalse(
176            symbolfile.symbol_in_api([Tag('introduced-arm=14')], Arch('arm'),
177                                     9))
178        self.assertFalse(
179            symbolfile.symbol_in_api([Tag('future')], Arch('arm'), 9))
180        self.assertFalse(
181            symbolfile.symbol_in_api(
182                [Tag('introduced=9'), Tag('future')], Arch('arm'), 14))
183        self.assertFalse(
184            symbolfile.symbol_in_api([Tag('introduced-arm=9'),
185                                      Tag('future')], Arch('arm'), 14))
186        self.assertFalse(
187            symbolfile.symbol_in_api(
188                [Tag('introduced-arm=21'),
189                 Tag('introduced-x86=9')], Arch('arm'), 14))
190        self.assertFalse(
191            symbolfile.symbol_in_api(
192                [Tag('introduced=9'),
193                 Tag('introduced-arm=21')], Arch('arm'), 14))
194        self.assertFalse(
195            symbolfile.symbol_in_api(
196                [Tag('introduced=21'),
197                 Tag('introduced-x86=9')], Arch('arm'), 14))
198
199        # Interesting edge case: this symbol should be omitted from the
200        # library, but this call should still return true because none of the
201        # tags indiciate that it's not present in this API level.
202        self.assertTrue(symbolfile.symbol_in_api([Tag('x86')], Arch('arm'), 9))
203
204    def test_verioned_in_api(self) -> None:
205        self.assertTrue(symbolfile.symbol_versioned_in_api([], 9))
206        self.assertTrue(
207            symbolfile.symbol_versioned_in_api([Tag('versioned=9')], 9))
208        self.assertTrue(
209            symbolfile.symbol_versioned_in_api([Tag('versioned=9')], 14))
210
211        self.assertFalse(
212            symbolfile.symbol_versioned_in_api([Tag('versioned=14')], 9))
213
214
215class OmitVersionTest(unittest.TestCase):
216    def setUp(self) -> None:
217        self.filter = Filter(arch = Arch('arm'), api = 9)
218        self.version = Version('foo', None, Tags(), [])
219
220    def assertOmit(self, f: Filter, v: Version) -> None:
221        self.assertTrue(f.should_omit_version(v))
222
223    def assertInclude(self, f: Filter, v: Version) -> None:
224        self.assertFalse(f.should_omit_version(v))
225
226    def test_omit_private(self) -> None:
227        f = self.filter
228        v = self.version
229
230        self.assertInclude(f, v)
231
232        v.name = 'foo_PRIVATE'
233        self.assertOmit(f, v)
234
235        v.name = 'foo_PLATFORM'
236        self.assertOmit(f, v)
237
238        v.name = 'foo'
239        v.tags = Tags.from_strs(['platform-only'])
240        self.assertOmit(f, v)
241
242    def test_omit_llndk(self) -> None:
243        f = self.filter
244        v = self.version
245        v_llndk = copy(v)
246        v_llndk.tags = Tags.from_strs(['llndk'])
247
248        self.assertOmit(f, v_llndk)
249
250        f.llndk = True
251        self.assertInclude(f, v)
252        self.assertInclude(f, v_llndk)
253
254    def test_omit_apex(self) -> None:
255        f = self.filter
256        v = self.version
257        v_apex = copy(v)
258        v_apex.tags = Tags.from_strs(['apex'])
259        v_systemapi = copy(v)
260        v_systemapi.tags = Tags.from_strs(['systemapi'])
261
262        self.assertOmit(f, v_apex)
263
264        f.apex = True
265        self.assertInclude(f, v)
266        self.assertInclude(f, v_apex)
267        self.assertOmit(f, v_systemapi)
268
269    def test_omit_systemapi(self) -> None:
270        f = self.filter
271        v = self.version
272        v_apex = copy(v)
273        v_apex.tags = Tags.from_strs(['apex'])
274        v_systemapi = copy(v)
275        v_systemapi.tags = Tags.from_strs(['systemapi'])
276
277        self.assertOmit(f, v_systemapi)
278
279        f.systemapi = True
280        self.assertInclude(f, v)
281        self.assertInclude(f, v_systemapi)
282        self.assertOmit(f, v_apex)
283
284    def test_omit_arch(self) -> None:
285        f_arm = self.filter
286        v_none = self.version
287        self.assertInclude(f_arm, v_none)
288
289        v_arm = copy(v_none)
290        v_arm.tags = Tags.from_strs(['arm'])
291        self.assertInclude(f_arm, v_arm)
292
293        v_x86 = copy(v_none)
294        v_x86.tags = Tags.from_strs(['x86'])
295        self.assertOmit(f_arm, v_x86)
296
297    def test_omit_api(self) -> None:
298        f_api9 = self.filter
299        v_none = self.version
300        self.assertInclude(f_api9, v_none)
301
302        v_api9 = copy(v_none)
303        v_api9.tags = Tags.from_strs(['introduced=9'])
304        self.assertInclude(f_api9, v_api9)
305
306        v_api14 = copy(v_none)
307        v_api14.tags = Tags.from_strs(['introduced=14'])
308        self.assertOmit(f_api9, v_api14)
309
310
311class OmitSymbolTest(unittest.TestCase):
312    def setUp(self) -> None:
313        self.filter = Filter(arch = Arch('arm'), api = 9)
314
315    def assertOmit(self, f: Filter, s: Symbol) -> None:
316        self.assertTrue(f.should_omit_symbol(s))
317
318    def assertInclude(self, f: Filter, s: Symbol) -> None:
319        self.assertFalse(f.should_omit_symbol(s))
320
321    def test_omit_ndk(self) -> None:
322        f_ndk = self.filter
323        f_nondk = copy(f_ndk)
324        f_nondk.ndk = False
325        f_nondk.apex = True
326
327        s_ndk = Symbol('foo', Tags())
328        s_nonndk = Symbol('foo', Tags.from_strs(['apex']))
329
330        self.assertInclude(f_ndk, s_ndk)
331        self.assertOmit(f_ndk, s_nonndk)
332        self.assertOmit(f_nondk, s_ndk)
333        self.assertInclude(f_nondk, s_nonndk)
334
335    def test_omit_llndk(self) -> None:
336        f_none = self.filter
337        f_llndk = copy(f_none)
338        f_llndk.llndk = True
339
340        s_none = Symbol('foo', Tags())
341        s_llndk = Symbol('foo', Tags.from_strs(['llndk']))
342
343        self.assertOmit(f_none, s_llndk)
344        self.assertInclude(f_llndk, s_none)
345        self.assertInclude(f_llndk, s_llndk)
346
347    def test_omit_llndk_versioned(self) -> None:
348        f_ndk = self.filter
349        f_ndk.api = 35
350
351        f_llndk = copy(f_ndk)
352        f_llndk.llndk = True
353        f_llndk.api = 202404
354
355        s = Symbol('foo', Tags())
356        s_llndk = Symbol('foo', Tags.from_strs(['llndk']))
357        s_llndk_202404 = Symbol('foo', Tags.from_strs(['llndk=202404']))
358        s_34 = Symbol('foo', Tags.from_strs(['introduced=34']))
359        s_34_llndk = Symbol('foo', Tags.from_strs(['introduced=34', 'llndk']))
360        s_35 = Symbol('foo', Tags.from_strs(['introduced=35']))
361        s_35_llndk_202404 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202404']))
362        s_35_llndk_202504 = Symbol('foo', Tags.from_strs(['introduced=35', 'llndk=202504']))
363
364        # When targeting NDK, omit LLNDK tags
365        self.assertInclude(f_ndk, s)
366        self.assertOmit(f_ndk, s_llndk)
367        self.assertOmit(f_ndk, s_llndk_202404)
368        self.assertInclude(f_ndk, s_34)
369        self.assertOmit(f_ndk, s_34_llndk)
370        self.assertInclude(f_ndk, s_35)
371        self.assertOmit(f_ndk, s_35_llndk_202404)
372        self.assertOmit(f_ndk, s_35_llndk_202504)
373
374        # When targeting LLNDK, old symbols without any mode tags are included as LLNDK
375        self.assertInclude(f_llndk, s)
376        # When targeting LLNDK, old symbols with #llndk are included as LLNDK
377        self.assertInclude(f_llndk, s_llndk)
378        self.assertInclude(f_llndk, s_llndk_202404)
379        self.assertInclude(f_llndk, s_34)
380        self.assertInclude(f_llndk, s_34_llndk)
381        # When targeting LLNDK, new symbols(>=35) should be tagged with llndk-introduced=.
382        self.assertOmit(f_llndk, s_35)
383        self.assertInclude(f_llndk, s_35_llndk_202404)
384        self.assertOmit(f_llndk, s_35_llndk_202504)
385
386    def test_omit_apex(self) -> None:
387        f_none = self.filter
388        f_apex = copy(f_none)
389        f_apex.apex = True
390
391        s_none = Symbol('foo', Tags())
392        s_apex = Symbol('foo', Tags.from_strs(['apex']))
393        s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
394
395        self.assertOmit(f_none, s_apex)
396        self.assertInclude(f_apex, s_none)
397        self.assertInclude(f_apex, s_apex)
398        self.assertOmit(f_apex, s_systemapi)
399
400    def test_omit_systemapi(self) -> None:
401        f_none = self.filter
402        f_systemapi = copy(f_none)
403        f_systemapi.systemapi = True
404
405        s_none = Symbol('foo', Tags())
406        s_apex = Symbol('foo', Tags.from_strs(['apex']))
407        s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
408
409        self.assertOmit(f_none, s_systemapi)
410        self.assertInclude(f_systemapi, s_none)
411        self.assertInclude(f_systemapi, s_systemapi)
412        self.assertOmit(f_systemapi, s_apex)
413
414    def test_omit_apex_and_systemapi(self) -> None:
415        f = self.filter
416        f.systemapi = True
417        f.apex = True
418
419        s_none = Symbol('foo', Tags())
420        s_apex = Symbol('foo', Tags.from_strs(['apex']))
421        s_systemapi = Symbol('foo', Tags.from_strs(['systemapi']))
422        self.assertInclude(f, s_none)
423        self.assertInclude(f, s_apex)
424        self.assertInclude(f, s_systemapi)
425
426    def test_omit_arch(self) -> None:
427        f_arm = self.filter
428        s_none = Symbol('foo', Tags())
429        s_arm = Symbol('foo', Tags.from_strs(['arm']))
430        s_x86 = Symbol('foo', Tags.from_strs(['x86']))
431
432        self.assertInclude(f_arm, s_none)
433        self.assertInclude(f_arm, s_arm)
434        self.assertOmit(f_arm, s_x86)
435
436    def test_omit_api(self) -> None:
437        f_api9 = self.filter
438        s_none = Symbol('foo', Tags())
439        s_api9 = Symbol('foo', Tags.from_strs(['introduced=9']))
440        s_api14 = Symbol('foo', Tags.from_strs(['introduced=14']))
441
442        self.assertInclude(f_api9, s_none)
443        self.assertInclude(f_api9, s_api9)
444        self.assertOmit(f_api9, s_api14)
445
446
447class SymbolFileParseTest(unittest.TestCase):
448    def setUp(self) -> None:
449        self.filter = Filter(arch = Arch('arm'), api = 16)
450
451    def test_next_line(self) -> None:
452        input_file = io.StringIO(textwrap.dedent("""\
453            foo
454
455            bar
456            # baz
457            qux
458        """))
459        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
460        self.assertIsNone(parser.current_line)
461
462        self.assertEqual('foo', parser.next_line().strip())
463        assert parser.current_line is not None
464        self.assertEqual('foo', parser.current_line.strip())
465
466        self.assertEqual('bar', parser.next_line().strip())
467        self.assertEqual('bar', parser.current_line.strip())
468
469        self.assertEqual('qux', parser.next_line().strip())
470        self.assertEqual('qux', parser.current_line.strip())
471
472        self.assertEqual('', parser.next_line())
473        self.assertEqual('', parser.current_line)
474
475    def test_parse_version(self) -> None:
476        input_file = io.StringIO(textwrap.dedent("""\
477            VERSION_1 { # weak introduced=35
478                baz;
479                qux; # apex llndk
480            };
481
482            VERSION_2 {
483            } VERSION_1; # not-a-tag
484        """))
485        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
486
487        parser.next_line()
488        version = parser.parse_version()
489        self.assertEqual('VERSION_1', version.name)
490        self.assertIsNone(version.base)
491        self.assertEqual(Tags.from_strs(['weak', 'introduced=35']), version.tags)
492
493        # Inherit introduced= tags from version block so that
494        # should_omit_tags() can differently based on introduced API level when treating
495        # LLNDK-available symbols.
496        expected_symbols = [
497            Symbol('baz', Tags.from_strs(['introduced=35'])),
498            Symbol('qux', Tags.from_strs(['apex', 'llndk', 'introduced=35'])),
499        ]
500        self.assertEqual(expected_symbols, version.symbols)
501
502        parser.next_line()
503        version = parser.parse_version()
504        self.assertEqual('VERSION_2', version.name)
505        self.assertEqual('VERSION_1', version.base)
506        self.assertEqual(Tags(), version.tags)
507
508    def test_parse_version_eof(self) -> None:
509        input_file = io.StringIO(textwrap.dedent("""\
510            VERSION_1 {
511        """))
512        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
513        parser.next_line()
514        with self.assertRaises(symbolfile.ParseError):
515            parser.parse_version()
516
517    def test_unknown_scope_label(self) -> None:
518        input_file = io.StringIO(textwrap.dedent("""\
519            VERSION_1 {
520                foo:
521            }
522        """))
523        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
524        parser.next_line()
525        with self.assertRaises(symbolfile.ParseError):
526            parser.parse_version()
527
528    def test_parse_symbol(self) -> None:
529        input_file = io.StringIO(textwrap.dedent("""\
530            foo;
531            bar; # llndk apex
532        """))
533        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
534
535        parser.next_line()
536        symbol = parser.parse_symbol()
537        self.assertEqual('foo', symbol.name)
538        self.assertEqual(Tags(), symbol.tags)
539
540        parser.next_line()
541        symbol = parser.parse_symbol()
542        self.assertEqual('bar', symbol.name)
543        self.assertEqual(Tags.from_strs(['llndk', 'apex']), symbol.tags)
544
545    def test_wildcard_symbol_global(self) -> None:
546        input_file = io.StringIO(textwrap.dedent("""\
547            VERSION_1 {
548                *;
549            };
550        """))
551        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
552        parser.next_line()
553        with self.assertRaises(symbolfile.ParseError):
554            parser.parse_version()
555
556    def test_wildcard_symbol_local(self) -> None:
557        input_file = io.StringIO(textwrap.dedent("""\
558            VERSION_1 {
559                local:
560                    *;
561            };
562        """))
563        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
564        parser.next_line()
565        version = parser.parse_version()
566        self.assertEqual([], version.symbols)
567
568    def test_missing_semicolon(self) -> None:
569        input_file = io.StringIO(textwrap.dedent("""\
570            VERSION_1 {
571                foo
572            };
573        """))
574        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
575        parser.next_line()
576        with self.assertRaises(symbolfile.ParseError):
577            parser.parse_version()
578
579    def test_parse_fails_invalid_input(self) -> None:
580        with self.assertRaises(symbolfile.ParseError):
581            input_file = io.StringIO('foo')
582            parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
583            parser.parse()
584
585    def test_parse(self) -> None:
586        input_file = io.StringIO(textwrap.dedent("""\
587            VERSION_1 {
588                local:
589                    hidden1;
590                global:
591                    foo;
592                    bar; # llndk
593            };
594
595            VERSION_2 { # weak
596                # Implicit global scope.
597                    woodly;
598                    doodly; # llndk
599                local:
600                    qwerty;
601            } VERSION_1;
602        """))
603        parser = symbolfile.SymbolFileParser(input_file, {}, self.filter)
604        versions = parser.parse()
605
606        expected = [
607            symbolfile.Version('VERSION_1', None, Tags(), [
608                Symbol('foo', Tags()),
609                Symbol('bar', Tags.from_strs(['llndk'])),
610            ]),
611            symbolfile.Version(
612                'VERSION_2', 'VERSION_1', Tags.from_strs(['weak']), [
613                    Symbol('woodly', Tags()),
614                    Symbol('doodly', Tags.from_strs(['llndk'])),
615                ]),
616        ]
617
618        self.assertEqual(expected, versions)
619
620    def test_parse_llndk_apex_symbol(self) -> None:
621        input_file = io.StringIO(textwrap.dedent("""\
622            VERSION_1 {
623                foo;
624                bar; # llndk
625                baz; # llndk apex
626                qux; # apex
627            };
628        """))
629        f = copy(self.filter)
630        f.llndk = True
631        parser = symbolfile.SymbolFileParser(input_file, {}, f)
632
633        parser.next_line()
634        version = parser.parse_version()
635        self.assertEqual('VERSION_1', version.name)
636        self.assertIsNone(version.base)
637
638        expected_symbols = [
639            Symbol('foo', Tags()),
640            Symbol('bar', Tags.from_strs(['llndk'])),
641            Symbol('baz', Tags.from_strs(['llndk', 'apex'])),
642            Symbol('qux', Tags.from_strs(['apex'])),
643        ]
644        self.assertEqual(expected_symbols, version.symbols)
645
646    def test_parse_llndk_version_is_missing(self) -> None:
647        input_file = io.StringIO(textwrap.dedent("""\
648            VERSION_1 { # introduced=35
649                foo;
650                bar; # llndk
651            };
652        """))
653        f = copy(self.filter)
654        f.llndk = True
655        parser = symbolfile.SymbolFileParser(input_file, {}, f)
656        with self.assertRaises(symbolfile.ParseError):
657            parser.parse()
658
659
660def main() -> None:
661    suite = unittest.TestLoader().loadTestsFromName(__name__)
662    unittest.TextTestRunner(verbosity=3).run(suite)
663
664
665if __name__ == '__main__':
666    main()
667