• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2012 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Tests for jni_zero.py.
6
7This test suite contains various tests for the JNI generator.
8It exercises the low-level parser all the way up to the
9code generator and ensures the output matches a golden
10file.
11"""
12
13import collections
14import copy
15import difflib
16import glob
17import logging
18import os
19import pathlib
20import re
21import shlex
22import subprocess
23import sys
24import tempfile
25import unittest
26import zipfile
27
28_SCRIPT_DIR = os.path.normpath(os.path.dirname(__file__))
29_GOLDENS_DIR = os.path.join(_SCRIPT_DIR, 'golden')
30_EXTRA_INCLUDES = 'third_party/jni_zero/jni_zero_helper.h'
31_JAVA_SRC_DIR = os.path.join(_SCRIPT_DIR, 'java', 'src', 'org', 'jni_zero')
32
33# Set this environment variable in order to regenerate the golden text
34# files.
35_REBASELINE = os.environ.get('REBASELINE', '0') != '0'
36
37_accessed_goldens = set()
38
39
40class CliOptions:
41  def __init__(self, is_final=False, is_javap=False, **kwargs):
42    if is_final:
43      self.action = 'generate-final'
44    elif is_javap:
45      self.action = 'from-jar'
46    else:
47      self.action = 'from-source'
48
49    self.input_files = []
50    self.jar_file = None
51    self.output_dir = None
52    self.output_files = None if is_final else []
53    self.header_path = None
54    self.enable_jni_multiplexing = False
55    self.package_prefix = None
56    self.package_prefix_filter = None
57    self.use_proxy_hash = False
58    self.extra_include = None if is_final else _EXTRA_INCLUDES
59    self.module_name = None
60    self.add_stubs_for_missing_native = False
61    self.include_test_only = False
62    self.manual_jni_registration = False
63    self.remove_uncalled_methods = False
64    self.__dict__.update(kwargs)
65
66  def to_args(self):
67    ret = [os.path.join(_SCRIPT_DIR, os.pardir, 'jni_zero.py'), self.action]
68    if self.enable_jni_multiplexing:
69      ret.append('--enable-jni-multiplexing')
70    if self.package_prefix:
71      ret += ['--package-prefix', self.package_prefix]
72    if self.package_prefix_filter:
73      ret += ['--package-prefix-filter', self.package_prefix_filter]
74    if self.use_proxy_hash:
75      ret.append('--use-proxy-hash')
76    if self.output_dir:
77      ret += ['--output-dir', self.output_dir]
78    if self.input_files:
79      for f in self.input_files:
80        ret += ['--input-file', f]
81    if self.output_files:
82      for f in self.output_files:
83        ret += ['--output-name', f]
84    if self.jar_file:
85      ret += ['--jar-file', self.jar_file]
86    if self.extra_include:
87      ret += ['--extra-include', self.extra_include]
88    if self.add_stubs_for_missing_native:
89      ret.append('--add-stubs-for-missing-native')
90    if self.header_path:
91      ret += ['--header-path', self.header_path]
92    if self.include_test_only:
93      ret.append('--include-test-only')
94    if self.manual_jni_registration:
95      ret.append('--manual-jni-registration')
96    if self.module_name:
97      ret += ['--module-name', self.module_name]
98    if self.remove_uncalled_methods:
99      ret.append('--remove-uncalled-methods')
100    return ret
101
102
103def _MakePrefixes(options):
104  package_prefix = ''
105  if options.package_prefix:
106    package_prefix = options.package_prefix.replace('.', '/') + '/'
107  module_prefix = ''
108  if options.module_name:
109    module_prefix = f'{options.module_name}_'
110  return package_prefix, module_prefix
111
112
113class BaseTest(unittest.TestCase):
114  def _CheckSrcjarGoldens(self, srcjar_path, name_to_goldens):
115    with zipfile.ZipFile(srcjar_path, 'r') as srcjar:
116      self.assertEqual(set(srcjar.namelist()), set(name_to_goldens))
117      for name in srcjar.namelist():
118        self.assertTrue(
119            name in name_to_goldens,
120            f'Found {name} output, but not present in name_to_goldens map.')
121        contents = srcjar.read(name).decode('utf-8')
122        self.AssertGoldenTextEquals(contents, name_to_goldens[name])
123
124  def _CheckPlaceholderSrcjarGolden(self, srcjar_path, golden_path):
125    expected_contents = [
126        'This is the concatenated contents of all files '
127        'inside the placeholder srcjar.\n\n'
128    ]
129    with zipfile.ZipFile(srcjar_path, 'r') as srcjar:
130      for name in srcjar.namelist():
131        file_contents = srcjar.read(name).decode('utf-8')
132        expected_contents += [f'## Contents of {name}:', file_contents, '\n']
133
134    self.AssertGoldenTextEquals('\n'.join(expected_contents), golden_path)
135
136  def _TestEndToEndGeneration(self,
137                              input_files,
138                              *,
139                              srcjar=False,
140                              generate_placeholders=False,
141                              enable_jni_multiplexing=False,
142                              per_file_natives=False,
143                              **kwargs):
144    is_javap = input_files[0].endswith('.class')
145    golden_name = self._testMethodName
146    options = CliOptions(is_javap=is_javap, **kwargs)
147    name_to_goldens = {}
148    if srcjar:
149      dir_prefix, file_prefix = _MakePrefixes(options)
150      # GEN_JNI ends up in placeholder srcjar instead if passed.
151      if not per_file_natives:
152        name_to_goldens.update({
153            f'{dir_prefix}org/jni_zero/{file_prefix}GEN_JNI.java':
154            f'{golden_name}-Placeholder-GEN_JNI.java.golden',
155        })
156    with tempfile.TemporaryDirectory() as tdir:
157      for i in input_files:
158        basename_and_folder = os.path.splitext(i)[0]
159        basename = os.path.basename(basename_and_folder)
160        options.output_files.append(f'{basename}_jni.h')
161        if srcjar:
162          name_to_goldens.update({
163              f'org/jni_zero/{basename_and_folder}Jni.java':
164              f'{golden_name}-{basename}Jni.java.golden',
165          })
166
167        relative_input_file = os.path.join(_JAVA_SRC_DIR, i)
168        if is_javap:
169          jar_path = os.path.join(tdir, 'input.jar')
170          with zipfile.ZipFile(jar_path, 'w') as z:
171            z.write(relative_input_file, i)
172          options.jar_file = jar_path
173          options.input_files.append(i)
174        else:
175          options.input_files.append(relative_input_file)
176
177      options.output_dir = tdir
178      cmd = options.to_args()
179
180      if srcjar:
181        srcjar_path = os.path.join(tdir, 'srcjar.jar')
182        cmd += ['--srcjar-path', srcjar_path]
183      if generate_placeholders:
184        placeholder_srcjar_path = os.path.join(tdir, 'placeholders.srcjar')
185        cmd += ['--placeholder-srcjar-path', placeholder_srcjar_path]
186      if enable_jni_multiplexing:
187        cmd += ['--enable-jni-multiplexing']
188      if per_file_natives:
189        cmd += ['--per-file-natives']
190
191      logging.info('Running: %s', shlex.join(cmd))
192      subprocess.check_call(cmd)
193
194      for o in options.output_files:
195        output_path = os.path.join(tdir, o)
196        with open(output_path, 'r') as f:
197          contents = f.read()
198          basename = os.path.splitext(o)[0]
199          header_golden = f'{golden_name}-{basename}.h.golden'
200          self.AssertGoldenTextEquals(contents, header_golden)
201
202      if srcjar:
203        self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
204      if generate_placeholders:
205        placeholder_srcjar_golden = f'{golden_name}-placeholder.srcjar.golden'
206        self._CheckPlaceholderSrcjarGolden(placeholder_srcjar_path,
207                                           placeholder_srcjar_golden)
208
209  def _TestEndToEndRegistration(self,
210                                input_files,
211                                golden_name=None,
212                                src_files_for_asserts_and_stubs=None,
213                                priority_java_files=None,
214                                inspection_func=None,
215                                **kwargs):
216    golden_name = golden_name or self._testMethodName
217    options = CliOptions(is_final=True, **kwargs)
218    dir_prefix, file_prefix = _MakePrefixes(options)
219    name_to_goldens = {
220        f'{dir_prefix}org/jni_zero/{file_prefix}GEN_JNI.java':
221        f'{golden_name}-Final-GEN_JNI.java.golden',
222    }
223    if options.use_proxy_hash or options.enable_jni_multiplexing:
224      name_to_goldens[f'{dir_prefix}J/{file_prefix}N.java'] = (
225          f'{golden_name}-Final-N.java.golden')
226    header_golden = None
227    if options.use_proxy_hash or options.manual_jni_registration or options.enable_jni_multiplexing:
228      header_golden = f'{golden_name}-Registration.h.golden'
229
230    with tempfile.TemporaryDirectory() as tdir:
231      native_sources = [os.path.join(_JAVA_SRC_DIR, f) for f in input_files]
232
233      if src_files_for_asserts_and_stubs:
234        java_sources = [
235            os.path.join(_JAVA_SRC_DIR, f)
236            for f in src_files_for_asserts_and_stubs
237        ]
238      else:
239        java_sources = native_sources
240
241      cmd = options.to_args()
242
243      java_sources_file = pathlib.Path(tdir) / 'java_sources.txt'
244      java_sources_file.write_text('\n'.join(java_sources))
245      cmd += ['--java-sources-file', str(java_sources_file)]
246      if native_sources:
247        native_sources_file = pathlib.Path(tdir) / 'native_sources.txt'
248        native_sources_file.write_text('\n'.join(native_sources))
249        cmd += ['--native-sources-file', str(native_sources_file)]
250      if priority_java_files:
251        priority_java_sources = [
252            os.path.join(_JAVA_SRC_DIR, f) for f in priority_java_files
253        ]
254        priority_java_file = pathlib.Path(tdir) / 'java_priority_sources.txt'
255        priority_java_file.write_text('\n'.join(priority_java_sources))
256        cmd += ['--priority-java-sources-file', str(priority_java_file)]
257      if priority_java_files is not None:
258        cmd += ['--never-omit-switch-num']
259
260      srcjar_path = os.path.join(tdir, 'srcjar.jar')
261      cmd += ['--srcjar-path', srcjar_path]
262      if header_golden:
263        header_path = os.path.join(tdir, 'header.h')
264        cmd += ['--header-path', header_path]
265
266      logging.info('Running: %s', shlex.join(cmd))
267      subprocess.check_call(cmd)
268
269      self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
270
271      if header_golden:
272        with open(header_path, 'r') as f:
273          # Temp directory will cause some diffs each time we run if we don't
274          # normalize.
275          contents = f.read().replace(
276              tdir.replace('/', '_').upper(), 'TEMP_DIR')
277          self.AssertGoldenTextEquals(contents, header_golden)
278      if inspection_func:
279        inspection_func(tdir)
280
281  def _TestParseError(self, error_snippet, input_data):
282    with tempfile.TemporaryDirectory() as tdir:
283      input_file = os.path.join(tdir, 'MyFile.java')
284      pathlib.Path(input_file).write_text(input_data)
285      options = CliOptions()
286      options.input_files = [input_file]
287      options.output_files = [f'{input_file}_jni.h']
288      options.output_dir = tdir
289      cmd = options.to_args()
290
291      logging.info('Running: %s', shlex.join(cmd))
292      result = subprocess.run(cmd, capture_output=True, check=False, text=True)
293      self.assertIn('MyFile.java', result.stderr)
294      self.assertIn(error_snippet, result.stderr)
295      self.assertEqual(result.returncode, 1)
296      return result.stderr
297
298  def _ReadGoldenFile(self, path):
299    _accessed_goldens.add(path)
300    if not os.path.exists(path):
301      return None
302    with open(path, 'r') as f:
303      return f.read()
304
305  def AssertTextEquals(self, golden_text, generated_text):
306    if not self.CompareText(golden_text, generated_text):
307      self.fail('Golden text mismatch.')
308
309  def CompareText(self, golden_text, generated_text):
310    def FilterText(text):
311      return [
312          l.strip() for l in text.split('\n')
313          if not l.startswith('// Copyright')
314      ]
315
316    stripped_golden = FilterText(golden_text)
317    stripped_generated = FilterText(generated_text)
318    if stripped_golden == stripped_generated:
319      return True
320    print(self.id())
321    for line in difflib.context_diff(stripped_golden, stripped_generated):
322      print(line)
323    print('\n\nGenerated')
324    print('=' * 80)
325    print(generated_text)
326    print('=' * 80)
327    print('Run with:')
328    print('REBASELINE=1', sys.argv[0])
329    print('to regenerate the data files.')
330
331  def AssertGoldenTextEquals(self, generated_text, golden_file):
332    """Compares generated text with the corresponding golden_file
333
334    It will instead compare the generated text with
335    script_dir/golden/golden_file."""
336    golden_path = os.path.join(_GOLDENS_DIR, golden_file)
337    golden_text = self._ReadGoldenFile(golden_path)
338    if _REBASELINE:
339      if golden_text != generated_text:
340        print('Updated', golden_path)
341        with open(golden_path, 'w') as f:
342          f.write(generated_text)
343      return
344    # golden_text is None if no file is found. Better to fail than in
345    # AssertTextEquals so we can give a clearer message.
346    if golden_text is None:
347      self.fail('Golden file does not exist: ' + golden_path)
348    self.AssertTextEquals(golden_text, generated_text)
349
350
351@unittest.skipIf(os.name == 'nt', 'Not intended to work on Windows')
352class Tests(BaseTest):
353  def testNonProxy(self):
354    self._TestEndToEndGeneration(['SampleNonProxy.java'])
355
356  def testBirectionalNonProxy(self):
357    self._TestEndToEndGeneration(['SampleBidirectionalNonProxy.java'])
358
359  def testBidirectionalClass(self):
360    self._TestEndToEndGeneration(['SampleForTests.java'], srcjar=True)
361    self._TestEndToEndRegistration(['SampleForTests.java'])
362
363  def testFromClassFile(self):
364    self._TestEndToEndGeneration(['JavapClass.class'])
365
366  def testUniqueAnnotations(self):
367    self._TestEndToEndGeneration(['SampleUniqueAnnotations.java'], srcjar=True)
368
369  def testPerFileNatives(self):
370    self._TestEndToEndGeneration(['SampleForAnnotationProcessor.java'],
371                                 srcjar=True,
372                                 per_file_natives=True)
373
374  def testEndToEndProxyHashed(self):
375    self._TestEndToEndGeneration(['SampleForAnnotationProcessor.java'],
376                                 srcjar=True,
377                                 generate_placeholders=True)
378    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
379                                   use_proxy_hash=True)
380
381  def testEndToEndManualRegistration(self):
382    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
383                                   manual_jni_registration=True)
384
385  def testEndToEndManualRegistration_NonProxy(self):
386    self._TestEndToEndRegistration(['SampleNonProxy.java'],
387                                   manual_jni_registration=True)
388
389  def testEndToEndProxyJniWithModules(self):
390    self._TestEndToEndGeneration(['SampleModule.java'],
391                                 srcjar=True,
392                                 use_proxy_hash=True,
393                                 module_name='module')
394    self._TestEndToEndRegistration(
395        ['SampleForAnnotationProcessor.java', 'SampleModule.java'],
396        use_proxy_hash=True,
397        manual_jni_registration=True,
398        module_name='module')
399
400  def testModulesWithMultiplexing(self):
401    self._TestEndToEndRegistration(
402        ['SampleForAnnotationProcessor.java', 'SampleModule.java'],
403        enable_jni_multiplexing=True,
404        manual_jni_registration=True,
405        module_name='module')
406
407  def testStubRegistration(self):
408    input_java_files = ['SampleForAnnotationProcessor.java']
409    stubs_java_files = input_java_files + [
410        'TinySample.java', 'SampleProxyEdgeCases.java'
411    ]
412    extra_input_java_files = ['TinySample2.java']
413    self._TestEndToEndRegistration(
414        input_java_files + extra_input_java_files,
415        src_files_for_asserts_and_stubs=stubs_java_files,
416        add_stubs_for_missing_native=True,
417        remove_uncalled_methods=True)
418
419  def testPriorityRegistration(self):
420    input_java_files = [
421        'TinySample2.java', 'TinySample.java', 'SampleProxyEdgeCases.java'
422    ]
423    # Add an entry not in input_java_files to simulate one that is in native
424    # sources but not java source (e.g. contains only @CalledByNative)
425    priority_java_files = ['TinySample2.java', 'SampleModule.java']
426
427    hash_holder = []
428
429    def inspection_func(tdir):
430      header_path = os.path.join(tdir, 'header.h')
431      header_text = pathlib.Path(header_path).read_text()
432      whole = re.findall(r'HashWhole.*?= (.*?);', header_text)[0]
433      priority = re.findall(r'HashPriority.*?= (.*?);', header_text)[0]
434      hash_holder.append((whole, priority))
435
436    self._TestEndToEndRegistration(input_java_files,
437                                   priority_java_files=priority_java_files,
438                                   inspection_func=inspection_func,
439                                   enable_jni_multiplexing=True)
440
441    self._TestEndToEndRegistration(priority_java_files,
442                                   golden_name='testPriorityRegistrationPart2',
443                                   priority_java_files=[],
444                                   inspection_func=inspection_func,
445                                   enable_jni_multiplexing=True)
446    self.assertEqual(hash_holder[0][1], hash_holder[1][0])
447
448  def testFullStubs(self):
449    self._TestEndToEndRegistration(
450        [],
451        src_files_for_asserts_and_stubs=['TinySample.java'],
452        add_stubs_for_missing_native=True)
453
454  def testForTestingKeptHash(self):
455    input_java_file = 'SampleProxyEdgeCases.java'
456    self._TestEndToEndGeneration([input_java_file], srcjar=True)
457    self._TestEndToEndRegistration([input_java_file],
458                                   use_proxy_hash=True,
459                                   include_test_only=True)
460
461  def testForTestingRemovedHash(self):
462    self._TestEndToEndRegistration(['SampleProxyEdgeCases.java'],
463                                   use_proxy_hash=True,
464                                   include_test_only=False)
465
466  def testForTestingKeptMultiplexing(self):
467    input_java_file = 'SampleProxyEdgeCases.java'
468    self._TestEndToEndGeneration([input_java_file], enable_jni_multiplexing=True, srcjar=True)
469    self._TestEndToEndRegistration([input_java_file],
470                                   enable_jni_multiplexing=True,
471                                   include_test_only=True)
472
473  def testForTestingRemovedMultiplexing(self):
474    self._TestEndToEndRegistration(['SampleProxyEdgeCases.java'],
475                                   enable_jni_multiplexing=True,
476                                   include_test_only=False)
477
478  def testPackagePrefixGenerator(self):
479    self._TestEndToEndGeneration(['SampleForTests.java'],
480                                 srcjar=True,
481                                 package_prefix='this.is.a.package.prefix')
482
483  def testPackagePrefixWithFilter(self):
484    self._TestEndToEndGeneration(['SampleForTests.java'],
485                                 srcjar=True,
486                                 package_prefix='this.is.a.package.prefix',
487                                 package_prefix_filter='org.jni_zero')
488
489  def testPackagePrefixWithManualRegistration(self):
490    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
491                                   package_prefix='this.is.a.package.prefix',
492                                   manual_jni_registration=True)
493
494  def testPackagePrefixWithMultiplexing(self):
495    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
496                                   package_prefix='this.is.a.package.prefix',
497                                   enable_jni_multiplexing=True)
498
499  def testPackagePrefixWithManualRegistrationWithMultiplexing(self):
500    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
501                                   package_prefix='this.is.a.package.prefix',
502                                   enable_jni_multiplexing=True,
503                                   manual_jni_registration=True)
504
505  def testPlaceholdersOverlapping(self):
506    self._TestEndToEndGeneration([
507        'TinySample.java',
508        'extrapackage/ImportsTinySample.java',
509    ],
510                                 srcjar=True,
511                                 generate_placeholders=True)
512
513  def testMultiplexing(self):
514    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
515                                   enable_jni_multiplexing=True,
516                                   manual_jni_registration=True)
517
518  def testParseError_noPackage(self):
519    data = """
520class MyFile {}
521"""
522    self._TestParseError('Unable to find "package" line', data)
523
524  def testParseError_noClass(self):
525    data = """
526package foo;
527"""
528    self._TestParseError('No classes found', data)
529
530  def testParseError_wrongClass(self):
531    data = """
532package foo;
533class YourFile {}
534"""
535    self._TestParseError('Found class "YourFile" but expected "MyFile"', data)
536
537  def testParseError_noMethods(self):
538    data = """
539package foo;
540class MyFile {
541  void foo() {}
542}
543"""
544    self._TestParseError('No native methods found', data)
545
546  def testParseError_noInterfaceMethods(self):
547    data = """
548package foo;
549class MyFile {
550  @NativeMethods
551  interface A {}
552}
553"""
554    self._TestParseError('Found no methods within', data)
555
556  def testParseError_twoInterfaces(self):
557    data = """
558package foo;
559class MyFile {
560  @NativeMethods
561  interface A {
562    void a();
563  }
564  @NativeMethods
565  interface B {
566    void b();
567  }
568}
569"""
570    self._TestParseError('Multiple @NativeMethod interfaces', data)
571
572  def testParseError_twoNamespaces(self):
573    data = """
574package foo;
575@JNINamespace("one")
576@JNINamespace("two")
577class MyFile {
578  @NativeMethods
579  interface A {
580    void a();
581  }
582}
583"""
584    self._TestParseError('Found multiple @JNINamespace', data)
585
586
587def main():
588  try:
589    unittest.main()
590  finally:
591    if _REBASELINE and not any(not x.startswith('-') for x in sys.argv[1:]):
592      for path in glob.glob(os.path.join(_GOLDENS_DIR, '*.golden')):
593        if path not in _accessed_goldens:
594          print('Removing obsolete golden:', path)
595          os.unlink(path)
596
597
598if __name__ == '__main__':
599  main()
600