1#!/usr/bin/env python
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17"""Tests for ndkstubgen.py."""
18import io
19import textwrap
20import unittest
21from copy import copy
22
23import symbolfile
24from symbolfile import Arch, Tags
25
26import ndkstubgen
27
28
29# pylint: disable=missing-docstring
30
31
32class GeneratorTest(unittest.TestCase):
33    def setUp(self) -> None:
34        self.filter = symbolfile.Filter(Arch('arm'), 9, False, False)
35
36    def test_omit_version(self) -> None:
37        # Thorough testing of the cases involved here is handled by
38        # OmitVersionTest, PrivateVersionTest, and SymbolPresenceTest.
39        src_file = io.StringIO()
40        version_file = io.StringIO()
41        symbol_list_file = io.StringIO()
42        generator = ndkstubgen.Generator(src_file,
43                                         version_file, symbol_list_file,
44                                         self.filter)
45
46        version = symbolfile.Version('VERSION_PRIVATE', None, Tags(), [
47            symbolfile.Symbol('foo', Tags()),
48        ])
49        generator.write_version(version)
50        self.assertEqual('', src_file.getvalue())
51        self.assertEqual('', version_file.getvalue())
52
53        version = symbolfile.Version('VERSION', None, Tags.from_strs(['x86']),
54                                     [
55                                         symbolfile.Symbol('foo', Tags()),
56                                     ])
57        generator.write_version(version)
58        self.assertEqual('', src_file.getvalue())
59        self.assertEqual('', version_file.getvalue())
60
61        version = symbolfile.Version('VERSION', None,
62                                     Tags.from_strs(['introduced=14']), [
63                                         symbolfile.Symbol('foo', Tags()),
64                                     ])
65        generator.write_version(version)
66        self.assertEqual('', src_file.getvalue())
67        self.assertEqual('', version_file.getvalue())
68
69    def test_omit_symbol(self) -> None:
70        # Thorough testing of the cases involved here is handled by
71        # SymbolPresenceTest.
72        src_file = io.StringIO()
73        version_file = io.StringIO()
74        symbol_list_file = io.StringIO()
75        generator = ndkstubgen.Generator(src_file,
76                                         version_file, symbol_list_file,
77                                         self.filter)
78
79        version = symbolfile.Version('VERSION_1', None, Tags(), [
80            symbolfile.Symbol('foo', Tags.from_strs(['x86'])),
81        ])
82        generator.write_version(version)
83        self.assertEqual('', src_file.getvalue())
84        self.assertEqual('', version_file.getvalue())
85
86        version = symbolfile.Version('VERSION_1', None, Tags(), [
87            symbolfile.Symbol('foo', Tags.from_strs(['introduced=14'])),
88        ])
89        generator.write_version(version)
90        self.assertEqual('', src_file.getvalue())
91        self.assertEqual('', version_file.getvalue())
92
93        version = symbolfile.Version('VERSION_1', None, Tags(), [
94            symbolfile.Symbol('foo', Tags.from_strs(['llndk'])),
95        ])
96        generator.write_version(version)
97        self.assertEqual('', src_file.getvalue())
98        self.assertEqual('', version_file.getvalue())
99
100        version = symbolfile.Version('VERSION_1', None, Tags(), [
101            symbolfile.Symbol('foo', Tags.from_strs(['apex'])),
102        ])
103        generator.write_version(version)
104        self.assertEqual('', src_file.getvalue())
105        self.assertEqual('', version_file.getvalue())
106
107    def test_write(self) -> None:
108        src_file = io.StringIO()
109        version_file = io.StringIO()
110        symbol_list_file = io.StringIO()
111        generator = ndkstubgen.Generator(src_file,
112                                         version_file, symbol_list_file,
113                                         self.filter)
114
115        versions = [
116            symbolfile.Version('VERSION_1', None, Tags(), [
117                symbolfile.Symbol('foo', Tags()),
118                symbolfile.Symbol('bar', Tags.from_strs(['var'])),
119                symbolfile.Symbol('woodly', Tags.from_strs(['weak'])),
120                symbolfile.Symbol('doodly', Tags.from_strs(['weak', 'var'])),
121            ]),
122            symbolfile.Version('VERSION_2', 'VERSION_1', Tags(), [
123                symbolfile.Symbol('baz', Tags()),
124            ]),
125            symbolfile.Version('VERSION_3', 'VERSION_1', Tags(), [
126                symbolfile.Symbol('qux', Tags.from_strs(['versioned=14'])),
127            ]),
128        ]
129
130        generator.write(versions)
131        expected_src = textwrap.dedent("""\
132            void foo() {}
133            int bar = 0;
134            __attribute__((weak)) void woodly() {}
135            __attribute__((weak)) int doodly = 0;
136            void baz() {}
137            void qux() {}
138        """)
139        self.assertEqual(expected_src, src_file.getvalue())
140
141        expected_version = textwrap.dedent("""\
142            VERSION_1 {
143                global:
144                    foo;
145                    bar;
146                    woodly;
147                    doodly;
148            };
149            VERSION_2 {
150                global:
151                    baz;
152            } VERSION_1;
153        """)
154        self.assertEqual(expected_version, version_file.getvalue())
155
156        expected_allowlist = textwrap.dedent("""\
157            [abi_symbol_list]
158            foo
159            bar
160            woodly
161            doodly
162            baz
163            qux
164        """)
165        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
166
167
168class IntegrationTest(unittest.TestCase):
169    def setUp(self) -> None:
170        self.filter = symbolfile.Filter(Arch('arm'), 9, False, False)
171
172    def test_integration(self) -> None:
173        api_map = {
174            'O': 9000,
175            'P': 9001,
176        }
177
178        input_file = io.StringIO(textwrap.dedent("""\
179            VERSION_1 {
180                global:
181                    foo; # var
182                    bar; # x86
183                    fizz; # introduced=O
184                    buzz; # introduced=P
185                local:
186                    *;
187            };
188
189            VERSION_2 { # arm
190                baz; # introduced=9
191                qux; # versioned=14
192            } VERSION_1;
193
194            VERSION_3 { # introduced=14
195                woodly;
196                doodly; # var
197            } VERSION_2;
198
199            VERSION_4 { # versioned=9
200                wibble;
201                wizzes; # llndk
202                waggle; # apex
203            } VERSION_2;
204
205            VERSION_5 { # versioned=14
206                wobble;
207            } VERSION_4;
208        """))
209        parser = symbolfile.SymbolFileParser(input_file, api_map, self.filter)
210        versions = parser.parse()
211
212        src_file = io.StringIO()
213        version_file = io.StringIO()
214        symbol_list_file = io.StringIO()
215        generator = ndkstubgen.Generator(src_file,
216                                         version_file, symbol_list_file,
217                                         self.filter)
218        generator.write(versions)
219
220        expected_src = textwrap.dedent("""\
221            int foo = 0;
222            void baz() {}
223            void qux() {}
224            void wibble() {}
225            void wobble() {}
226        """)
227        self.assertEqual(expected_src, src_file.getvalue())
228
229        expected_version = textwrap.dedent("""\
230            VERSION_1 {
231                global:
232                    foo;
233            };
234            VERSION_2 {
235                global:
236                    baz;
237            } VERSION_1;
238            VERSION_4 {
239                global:
240                    wibble;
241            } VERSION_2;
242        """)
243        self.assertEqual(expected_version, version_file.getvalue())
244
245        expected_allowlist = textwrap.dedent("""\
246            [abi_symbol_list]
247            foo
248            baz
249            qux
250            wibble
251            wobble
252        """)
253        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
254
255    def test_integration_future_api(self) -> None:
256        api_map = {
257            'O': 9000,
258            'P': 9001,
259            'Q': 9002,
260        }
261
262        input_file = io.StringIO(textwrap.dedent("""\
263            VERSION_1 {
264                global:
265                    foo; # introduced=O
266                    bar; # introduced=P
267                    baz; # introduced=Q
268                local:
269                    *;
270            };
271        """))
272        f = copy(self.filter)
273        f.api = 9001
274        parser = symbolfile.SymbolFileParser(input_file, api_map, f)
275        versions = parser.parse()
276
277        src_file = io.StringIO()
278        version_file = io.StringIO()
279        symbol_list_file = io.StringIO()
280        f = copy(self.filter)
281        f.api = 9001
282        generator = ndkstubgen.Generator(src_file,
283                                         version_file, symbol_list_file, f)
284        generator.write(versions)
285
286        expected_src = textwrap.dedent("""\
287            void foo() {}
288            void bar() {}
289        """)
290        self.assertEqual(expected_src, src_file.getvalue())
291
292        expected_version = textwrap.dedent("""\
293            VERSION_1 {
294                global:
295                    foo;
296                    bar;
297            };
298        """)
299        self.assertEqual(expected_version, version_file.getvalue())
300
301        expected_allowlist = textwrap.dedent("""\
302            [abi_symbol_list]
303            foo
304            bar
305        """)
306        self.assertEqual(expected_allowlist, symbol_list_file.getvalue())
307
308    def test_multiple_definition(self) -> None:
309        input_file = io.StringIO(textwrap.dedent("""\
310            VERSION_1 {
311                global:
312                    foo;
313                    foo;
314                    bar;
315                    baz;
316                    qux; # arm
317                local:
318                    *;
319            };
320
321            VERSION_2 {
322                global:
323                    bar;
324                    qux; # arm64
325            } VERSION_1;
326
327            VERSION_PRIVATE {
328                global:
329                    baz;
330            } VERSION_2;
331
332        """))
333        f = copy(self.filter)
334        f.api = 16
335        parser = symbolfile.SymbolFileParser(input_file, {}, f)
336
337        with self.assertRaises(
338                symbolfile.MultiplyDefinedSymbolError) as ex_context:
339            parser.parse()
340        self.assertEqual(['bar', 'foo'],
341                         ex_context.exception.multiply_defined_symbols)
342
343    def test_integration_with_apex(self) -> None:
344        api_map = {
345            'O': 9000,
346            'P': 9001,
347        }
348
349        input_file = io.StringIO(textwrap.dedent("""\
350            VERSION_1 {
351                global:
352                    foo; # var
353                    bar; # x86
354                    fizz; # introduced=O
355                    buzz; # introduced=P
356                local:
357                    *;
358            };
359
360            VERSION_2 { # arm
361                baz; # introduced=9
362                qux; # versioned=14
363            } VERSION_1;
364
365            VERSION_3 { # introduced=14
366                woodly;
367                doodly; # var
368            } VERSION_2;
369
370            VERSION_4 { # versioned=9
371                wibble;
372                wizzes; # llndk
373                waggle; # apex
374                bubble; # apex llndk
375                duddle; # llndk apex
376            } VERSION_2;
377
378            VERSION_5 { # versioned=14
379                wobble;
380            } VERSION_4;
381        """))
382        f = copy(self.filter)
383        f.apex = True
384        parser = symbolfile.SymbolFileParser(input_file, api_map, f)
385        versions = parser.parse()
386
387        src_file = io.StringIO()
388        version_file = io.StringIO()
389        symbol_list_file = io.StringIO()
390        f = copy(self.filter)
391        f.apex = True
392        generator = ndkstubgen.Generator(src_file,
393                                         version_file, symbol_list_file, f)
394        generator.write(versions)
395
396        expected_src = textwrap.dedent("""\
397            int foo = 0;
398            void baz() {}
399            void qux() {}
400            void wibble() {}
401            void waggle() {}
402            void bubble() {}
403            void duddle() {}
404            void wobble() {}
405        """)
406        self.assertEqual(expected_src, src_file.getvalue())
407
408        expected_version = textwrap.dedent("""\
409            VERSION_1 {
410                global:
411                    foo;
412            };
413            VERSION_2 {
414                global:
415                    baz;
416            } VERSION_1;
417            VERSION_4 {
418                global:
419                    wibble;
420                    waggle;
421                    bubble;
422                    duddle;
423            } VERSION_2;
424        """)
425        self.assertEqual(expected_version, version_file.getvalue())
426
427    def test_integration_with_nondk(self) -> None:
428        input_file = io.StringIO(textwrap.dedent("""\
429            VERSION_1 {
430                global:
431                    foo;
432                    bar; # apex
433                local:
434                    *;
435            };
436        """))
437        f = copy(self.filter)
438        f.apex = True
439        f.ndk = False   # ndk symbols should be excluded
440        parser = symbolfile.SymbolFileParser(input_file, {}, f)
441        versions = parser.parse()
442
443        src_file = io.StringIO()
444        version_file = io.StringIO()
445        symbol_list_file = io.StringIO()
446        f = copy(self.filter)
447        f.apex = True
448        f.ndk = False   # ndk symbols should be excluded
449        generator = ndkstubgen.Generator(src_file,
450                                         version_file, symbol_list_file, f)
451        generator.write(versions)
452
453        expected_src = textwrap.dedent("""\
454            void bar() {}
455        """)
456        self.assertEqual(expected_src, src_file.getvalue())
457
458        expected_version = textwrap.dedent("""\
459            VERSION_1 {
460                global:
461                    bar;
462            };
463        """)
464        self.assertEqual(expected_version, version_file.getvalue())
465
466    def test_integration_with_llndk(self) -> None:
467        input_file = io.StringIO(textwrap.dedent("""\
468            VERSION_34 { # introduced=34
469                global:
470                    foo;
471                    bar; # llndk
472            };
473            VERSION_35 { # introduced=35
474                global:
475                    wiggle;
476                    waggle;
477                    waggle; # llndk=202404
478                    bubble; # llndk=202404
479                    duddle;
480                    duddle; # llndk=202504
481            } VERSION_34;
482        """))
483        f = copy(self.filter)
484        f.llndk = True
485        f.api = 202404
486        parser = symbolfile.SymbolFileParser(input_file, {}, f)
487        versions = parser.parse()
488
489        src_file = io.StringIO()
490        version_file = io.StringIO()
491        symbol_list_file = io.StringIO()
492
493        generator = ndkstubgen.Generator(src_file,
494                                         version_file, symbol_list_file, f)
495        generator.write(versions)
496
497        expected_src = textwrap.dedent("""\
498            void foo() {}
499            void bar() {}
500            void waggle() {}
501            void bubble() {}
502        """)
503        self.assertEqual(expected_src, src_file.getvalue())
504
505        expected_version = textwrap.dedent("""\
506            VERSION_34 {
507                global:
508                    foo;
509                    bar;
510            };
511            VERSION_35 {
512                global:
513                    waggle;
514                    bubble;
515            } VERSION_34;
516        """)
517        self.assertEqual(expected_version, version_file.getvalue())
518
519    def test_integration_with_llndk_with_single_version_block(self) -> None:
520        input_file = io.StringIO(textwrap.dedent("""\
521            LIBANDROID {
522                global:
523                    foo; # introduced=34
524                    bar; # introduced=35
525                    bar; # llndk=202404
526                    baz; # introduced=35
527            };
528        """))
529        f = copy(self.filter)
530        f.llndk = True
531        f.api = 202404
532        parser = symbolfile.SymbolFileParser(input_file, {}, f)
533        versions = parser.parse()
534
535        src_file = io.StringIO()
536        version_file = io.StringIO()
537        symbol_list_file = io.StringIO()
538
539        generator = ndkstubgen.Generator(src_file,
540                                         version_file, symbol_list_file, f)
541        generator.write(versions)
542
543        expected_src = textwrap.dedent("""\
544            void foo() {}
545            void bar() {}
546        """)
547        self.assertEqual(expected_src, src_file.getvalue())
548
549        expected_version = textwrap.dedent("""\
550            LIBANDROID {
551                global:
552                    foo;
553                    bar;
554            };
555        """)
556        self.assertEqual(expected_version, version_file.getvalue())
557
558    def test_empty_stub(self) -> None:
559        """Tests that empty stubs can be generated.
560
561        This is not a common case, but libraries whose only behavior is to
562        interpose symbols to alter existing behavior do not need to expose
563        their interposing symbols as API, so it's possible for the stub to be
564        empty while still needing a stub to link against. libsigchain is an
565        example of this.
566        """
567        input_file = io.StringIO(textwrap.dedent("""\
568            VERSION_1 {
569                local:
570                    *;
571            };
572        """))
573        f = copy(self.filter)
574        f.apex = True
575        parser = symbolfile.SymbolFileParser(input_file, {}, f)
576        versions = parser.parse()
577
578        src_file = io.StringIO()
579        version_file = io.StringIO()
580        symbol_list_file = io.StringIO()
581        f = copy(self.filter)
582        f.apex = True
583        generator = ndkstubgen.Generator(src_file,
584                                         version_file,
585                                         symbol_list_file, f)
586        generator.write(versions)
587
588        self.assertEqual('', src_file.getvalue())
589        self.assertEqual('', version_file.getvalue())
590
591
592def main() -> None:
593    suite = unittest.TestLoader().loadTestsFromName(__name__)
594    unittest.TextTestRunner(verbosity=3).run(suite)
595
596
597if __name__ == '__main__':
598    main()
599