• 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 shlex
21import subprocess
22import sys
23import tempfile
24import unittest
25import zipfile
26
27_SCRIPT_DIR = os.path.normpath(os.path.dirname(__file__))
28_GOLDENS_DIR = os.path.join(_SCRIPT_DIR, 'golden')
29_EXTRA_INCLUDES = 'third_party/jni_zero/jni_zero_helper.h'
30_JAVA_SRC_DIR = os.path.join(_SCRIPT_DIR, 'samples', 'java', 'src', 'org',
31                             'jni_zero', 'samples')
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_file = None
50    self.jar_file = None
51    self.output_dir = None
52    self.output_name = None if is_final else 'output.h'
53    self.header_path = None
54    self.enable_jni_multiplexing = False
55    self.package_prefix = None
56    self.use_proxy_hash = False
57    self.extra_include = None if is_final else _EXTRA_INCLUDES
58    self.module_name = None
59    self.add_stubs_for_missing_native = False
60    self.enable_proxy_mocks = False
61    self.include_test_only = False
62    self.manual_jni_registration = False
63    self.remove_uncalled_methods = False
64    self.require_mocks = False
65    self.__dict__.update(kwargs)
66
67  def to_args(self):
68    ret = [os.path.join(_SCRIPT_DIR, 'jni_zero.py'), self.action]
69    if self.enable_jni_multiplexing:
70      ret.append('--enable-jni-multiplexing')
71    if self.package_prefix:
72      ret += ['--package-prefix', self.package_prefix]
73    if self.use_proxy_hash:
74      ret.append('--use-proxy-hash')
75    if self.output_dir:
76      ret += ['--output-dir', self.output_dir]
77    if self.input_file:
78      ret += ['--input-file', self.input_file]
79    if self.output_name:
80      ret += ['--output-name', self.output_name]
81    if self.jar_file:
82      ret += ['--jar-file', self.jar_file]
83    if self.extra_include:
84      ret += ['--extra-include', self.extra_include]
85    if self.add_stubs_for_missing_native:
86      ret.append('--add-stubs-for-missing-native')
87    if self.enable_proxy_mocks:
88      ret.append('--enable-proxy-mocks')
89    if self.header_path:
90      ret += ['--header-path', self.header_path]
91    if self.include_test_only:
92      ret.append('--include-test-only')
93    if self.manual_jni_registration:
94      ret.append('--manual-jni-registration')
95    if self.module_name:
96      ret += ['--module-name', self.module_name]
97    if self.remove_uncalled_methods:
98      ret.append('--remove-uncalled-methods')
99    if self.require_mocks:
100      ret.append('--require-mocks')
101    return ret
102
103
104def _MakePrefixes(options):
105  package_prefix = ''
106  if options.package_prefix:
107    package_prefix = options.package_prefix.replace('.', '/') + '/'
108  module_prefix = ''
109  if options.module_name:
110    module_prefix = f'{options.module_name}_'
111  return package_prefix, module_prefix
112
113
114class BaseTest(unittest.TestCase):
115  def _CheckSrcjarGoldens(self, srcjar_path, name_to_goldens):
116    with zipfile.ZipFile(srcjar_path, 'r') as srcjar:
117      self.assertEqual(set(srcjar.namelist()), set(name_to_goldens))
118      for name in srcjar.namelist():
119        self.assertTrue(
120            name in name_to_goldens,
121            f'Found {name} output, but not present in name_to_goldens map.')
122        contents = srcjar.read(name).decode('utf-8')
123        self.AssertGoldenTextEquals(contents, name_to_goldens[name])
124
125  def _TestEndToEndGeneration(self, input_file, *, srcjar=False, **kwargs):
126    is_javap = input_file.endswith('.class')
127    golden_name = self._testMethodName
128    options = CliOptions(is_javap=is_javap, **kwargs)
129    basename = os.path.splitext(input_file)[0]
130    header_golden = f'{golden_name}-{basename}_jni.h.golden'
131    if srcjar:
132      dir_prefix, file_prefix = _MakePrefixes(options)
133      name_to_goldens = {
134          f'{dir_prefix}org/jni_zero/{file_prefix}GEN_JNI.java':
135          f'{golden_name}-Placeholder-GEN_JNI.java.golden',
136          f'org/jni_zero/samples/{basename}Jni.java':
137          f'{golden_name}-{basename}Jni.java.golden',
138      }
139
140    with tempfile.TemporaryDirectory() as tdir:
141      relative_input_file = os.path.join(_JAVA_SRC_DIR, input_file)
142      if is_javap:
143        jar_path = os.path.join(tdir, 'input.jar')
144        with zipfile.ZipFile(jar_path, 'w') as z:
145          z.write(relative_input_file, input_file)
146        options.jar_file = jar_path
147        options.input_file = input_file
148      else:
149        options.input_file = relative_input_file
150
151      options.output_dir = tdir
152      cmd = options.to_args()
153      if srcjar:
154        srcjar_path = os.path.join(tdir, 'srcjar.jar')
155        cmd += ['--srcjar-path', srcjar_path]
156
157      logging.info('Running: %s', shlex.join(cmd))
158      subprocess.check_call(cmd)
159
160      output_path = os.path.join(tdir, options.output_name)
161      with open(output_path, 'r') as f:
162        contents = f.read()
163      self.AssertGoldenTextEquals(contents, header_golden)
164
165      if srcjar:
166        self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
167
168  def _TestEndToEndRegistration(self,
169                                input_files,
170                                src_files_for_asserts_and_stubs=None,
171                                **kwargs):
172    golden_name = self._testMethodName
173    options = CliOptions(is_final=True, **kwargs)
174    dir_prefix, file_prefix = _MakePrefixes(options)
175    name_to_goldens = {
176        f'{dir_prefix}org/jni_zero/{file_prefix}GEN_JNI.java':
177        f'{golden_name}-Final-GEN_JNI.java.golden',
178    }
179    if options.use_proxy_hash:
180      name_to_goldens[f'{dir_prefix}J/{file_prefix}N.java'] = (
181          f'{golden_name}-Final-N.java.golden')
182    header_golden = None
183    if options.use_proxy_hash or options.manual_jni_registration:
184      header_golden = f'{golden_name}-Registration.h.golden'
185
186    with tempfile.TemporaryDirectory() as tdir:
187      native_sources = [os.path.join(_JAVA_SRC_DIR, f) for f in input_files]
188
189      if src_files_for_asserts_and_stubs:
190        java_sources = [
191            os.path.join(_JAVA_SRC_DIR, f)
192            for f in src_files_for_asserts_and_stubs
193        ]
194      else:
195        java_sources = native_sources
196
197      cmd = options.to_args()
198
199      java_sources_file = pathlib.Path(tdir) / 'java_sources.txt'
200      java_sources_file.write_text('\n'.join(java_sources))
201      cmd += ['--java-sources-file', str(java_sources_file)]
202      if native_sources:
203        native_sources_file = pathlib.Path(tdir) / 'native_sources.txt'
204        native_sources_file.write_text('\n'.join(native_sources))
205        cmd += ['--native-sources-file', str(native_sources_file)]
206
207      srcjar_path = os.path.join(tdir, 'srcjar.jar')
208      cmd += ['--srcjar-path', srcjar_path]
209      if header_golden:
210        header_path = os.path.join(tdir, 'header.h')
211        cmd += ['--header-path', header_path]
212
213      logging.info('Running: %s', shlex.join(cmd))
214      subprocess.check_call(cmd)
215
216      self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
217
218      if header_golden:
219        with open(header_path, 'r') as f:
220          # Temp directory will cause some diffs each time we run if we don't
221          # normalize.
222          contents = f.read().replace(
223              tdir.replace('/', '_').upper(), 'TEMP_DIR')
224          self.AssertGoldenTextEquals(contents, header_golden)
225
226  def _TestParseError(self, error_snippet, input_data):
227    with tempfile.TemporaryDirectory() as tdir:
228      input_file = os.path.join(tdir, 'MyFile.java')
229      pathlib.Path(input_file).write_text(input_data)
230      options = CliOptions()
231      options.input_file = input_file
232      options.output_dir = tdir
233      cmd = options.to_args()
234
235      logging.info('Running: %s', shlex.join(cmd))
236      result = subprocess.run(cmd, capture_output=True, check=False, text=True)
237      self.assertEqual(result.returncode, 1)
238      self.assertIn('MyFile.java', result.stderr)
239      self.assertIn(error_snippet, result.stderr)
240      return result.stderr
241
242  def _ReadGoldenFile(self, path):
243    _accessed_goldens.add(path)
244    if not os.path.exists(path):
245      return None
246    with open(path, 'r') as f:
247      return f.read()
248
249  def AssertTextEquals(self, golden_text, generated_text):
250    if not self.CompareText(golden_text, generated_text):
251      self.fail('Golden text mismatch.')
252
253  def CompareText(self, golden_text, generated_text):
254    def FilterText(text):
255      return [
256          l.strip() for l in text.split('\n')
257          if not l.startswith('// Copyright')
258      ]
259
260    stripped_golden = FilterText(golden_text)
261    stripped_generated = FilterText(generated_text)
262    if stripped_golden == stripped_generated:
263      return True
264    print(self.id())
265    for line in difflib.context_diff(stripped_golden, stripped_generated):
266      print(line)
267    print('\n\nGenerated')
268    print('=' * 80)
269    print(generated_text)
270    print('=' * 80)
271    print('Run with:')
272    print('REBASELINE=1', sys.argv[0])
273    print('to regenerate the data files.')
274
275  def AssertGoldenTextEquals(self, generated_text, golden_file):
276    """Compares generated text with the corresponding golden_file
277
278    It will instead compare the generated text with
279    script_dir/golden/golden_file."""
280    golden_path = os.path.join(_GOLDENS_DIR, golden_file)
281    golden_text = self._ReadGoldenFile(golden_path)
282    if _REBASELINE:
283      if golden_text != generated_text:
284        print('Updated', golden_path)
285        with open(golden_path, 'w') as f:
286          f.write(generated_text)
287      return
288    # golden_text is None if no file is found. Better to fail than in
289    # AssertTextEquals so we can give a clearer message.
290    if golden_text is None:
291      self.fail('Golden file does not exist: ' + golden_path)
292    self.AssertTextEquals(golden_text, generated_text)
293
294
295@unittest.skipIf(os.name == 'nt', 'Not intended to work on Windows')
296class Tests(BaseTest):
297  def testNonProxy(self):
298    self._TestEndToEndGeneration('SampleNonProxy.java')
299
300  def testBirectionalNonProxy(self):
301    self._TestEndToEndGeneration('SampleBidirectionalNonProxy.java')
302
303  def testBidirectionalClass(self):
304    self._TestEndToEndGeneration('SampleForTests.java', srcjar=True)
305    self._TestEndToEndRegistration(['SampleForTests.java'])
306
307  def testFromClassFile(self):
308    self._TestEndToEndGeneration('JavapClass.class')
309
310  def testUniqueAnnotations(self):
311    self._TestEndToEndGeneration('SampleUniqueAnnotations.java', srcjar=True)
312
313  def testEndToEndProxyHashed(self):
314    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
315                                   use_proxy_hash=True)
316
317  def testEndToEndManualRegistration(self):
318    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
319                                   manual_jni_registration=True)
320
321  def testEndToEndManualRegistration_NonProxy(self):
322    self._TestEndToEndRegistration(['SampleNonProxy.java'],
323                                   manual_jni_registration=True)
324
325  def testEndToEndProxyJniWithModules(self):
326    self._TestEndToEndGeneration('SampleModule.java',
327                                 srcjar=True,
328                                 use_proxy_hash=True,
329                                 module_name='module')
330    self._TestEndToEndRegistration(
331        ['SampleForAnnotationProcessor.java', 'SampleModule.java'],
332        use_proxy_hash=True,
333        module_name='module')
334
335  def testStubRegistration(self):
336    input_java_files = ['SampleForAnnotationProcessor.java']
337    stubs_java_files = input_java_files + [
338        'TinySample.java', 'SampleProxyEdgeCases.java'
339    ]
340    extra_input_java_files = ['TinySample2.java']
341    self._TestEndToEndRegistration(
342        input_java_files + extra_input_java_files,
343        src_files_for_asserts_and_stubs=stubs_java_files,
344        add_stubs_for_missing_native=True,
345        remove_uncalled_methods=True)
346
347  def testFullStubs(self):
348    self._TestEndToEndRegistration(
349        [],
350        src_files_for_asserts_and_stubs=['TinySample.java'],
351        add_stubs_for_missing_native=True)
352
353  def testForTestingKept(self):
354    input_java_file = 'SampleProxyEdgeCases.java'
355    self._TestEndToEndGeneration(input_java_file, srcjar=True)
356    self._TestEndToEndRegistration([input_java_file],
357                                   use_proxy_hash=True,
358                                   include_test_only=True)
359
360  def testForTestingRemoved(self):
361    self._TestEndToEndRegistration(['SampleProxyEdgeCases.java'],
362                                   use_proxy_hash=True,
363                                   include_test_only=True)
364
365  def testProxyMocks(self):
366    self._TestEndToEndRegistration(['TinySample.java'], enable_proxy_mocks=True)
367
368  def testRequireProxyMocks(self):
369    self._TestEndToEndRegistration(['TinySample.java'],
370                                   enable_proxy_mocks=True,
371                                   require_mocks=True)
372
373  def testPackagePrefixGenerator(self):
374    self._TestEndToEndGeneration('SampleForTests.java',
375                                 srcjar=True,
376                                 package_prefix='this.is.a.package.prefix')
377
378  def testPackagePrefixWithManualRegistration(self):
379    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
380                                   package_prefix='this.is.a.package.prefix',
381                                   manual_jni_registration=True)
382
383  def testPackagePrefixWithProxyHash(self):
384    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
385                                   package_prefix='this.is.a.package.prefix',
386                                   use_proxy_hash=True)
387
388  def testPackagePrefixWithManualRegistrationWithProxyHash(self):
389    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
390                                   package_prefix='this.is.a.package.prefix',
391                                   use_proxy_hash=True,
392                                   manual_jni_registration=True)
393
394  def testMultiplexing(self):
395    self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
396                                   enable_jni_multiplexing=True,
397                                   use_proxy_hash=True)
398
399  def testParseError_noPackage(self):
400    data = """
401class MyFile {}
402"""
403    self._TestParseError('Unable to find "package" line', data)
404
405  def testParseError_noClass(self):
406    data = """
407package foo;
408"""
409    self._TestParseError('No classes found', data)
410
411  def testParseError_wrongClass(self):
412    data = """
413package foo;
414class YourFile {}
415"""
416    self._TestParseError('Found class "YourFile" but expected "MyFile"', data)
417
418  def testParseError_noMethods(self):
419    data = """
420package foo;
421class MyFile {
422  void foo() {}
423}
424"""
425    self._TestParseError('No native methods found', data)
426
427  def testParseError_noInterfaceMethods(self):
428    data = """
429package foo;
430class MyFile {
431  @NativeMethods
432  interface A {}
433}
434"""
435    self._TestParseError('Found no methods within', data)
436
437  def testParseError_twoInterfaces(self):
438    data = """
439package foo;
440class MyFile {
441  @NativeMethods
442  interface A {
443    void a();
444  }
445  @NativeMethods
446  interface B {
447    void b();
448  }
449}
450"""
451    self._TestParseError('Multiple @NativeMethod interfaces', data)
452
453  def testParseError_twoNamespaces(self):
454    data = """
455package foo;
456@JNINamespace("one")
457@JNINamespace("two")
458class MyFile {
459  @NativeMethods
460  interface A {
461    void a();
462  }
463}
464"""
465    self._TestParseError('Found multiple @JNINamespace', data)
466
467
468def main():
469  try:
470    unittest.main()
471  finally:
472    if _REBASELINE and not any(not x.startswith('-') for x in sys.argv[1:]):
473      for path in glob.glob(os.path.join(_GOLDENS_DIR, '*.golden')):
474        if path not in _accessed_goldens:
475          print('Removing obsolete golden:', path)
476          os.unlink(path)
477
478
479if __name__ == '__main__':
480  main()
481