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