blob: eca48a14d029fb97d997b39ba121a283f6fdc8b1 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for jni_generator.py.
This test suite contains various tests for the JNI generator.
It exercises the low-level parser all the way up to the
code generator and ensures the output matches a golden
file.
"""
import collections
import copy
import difflib
import glob
import logging
import os
import pathlib
import shlex
import subprocess
import sys
import tempfile
import unittest
import zipfile
_SCRIPT_DIR = os.path.normpath(os.path.dirname(__file__))
_GOLDENS_DIR = os.path.join(_SCRIPT_DIR, 'golden')
_INCLUDES = ('base/android/jni_generator/jni_generator_helper.h')
_JAVA_SRC_DIR = os.path.join(_SCRIPT_DIR, 'java', 'src', 'org', 'chromium',
'example', 'jni_generator')
# Set this environment variable in order to regenerate the golden text
# files.
_REBASELINE = os.environ.get('REBASELINE', '0') != '0'
_accessed_goldens = set()
class CommonOptions:
def __init__(self):
self.enable_jni_multiplexing = False
self.package_prefix = None
self.use_proxy_hash = False
def to_args(self):
ret = []
if self.package_prefix:
ret += ['--package_prefix', self.package_prefix]
return ret
class JniGeneratorOptions(CommonOptions):
def __init__(self, **kwargs):
super().__init__()
self.includes = _INCLUDES
self.input_file = None
self.jar_file = None
self.module_name = ''
self.output_dir = None
self.output_name = 'output.h'
self.__dict__.update(kwargs)
def to_args(self):
ret = super().to_args()
ret += ['--output_dir', self.output_dir]
ret += ['--input_file', self.input_file]
ret += ['--output_name', self.output_name]
if self.jar_file:
ret += ['--jar_file', self.jar_file]
if self.enable_jni_multiplexing:
ret.append('--enable_jni_multiplexing')
if self.includes:
ret += ['--includes', self.includes, '--ptr_type', 'long']
if self.use_proxy_hash:
ret.append('--use_proxy_hash')
return ret
class JniRegistrationGeneratorOptions(CommonOptions):
"""The mock options object which is passed to the jni_generator.py script."""
def __init__(self, **kwargs):
super().__init__()
self.add_stubs_for_missing_native = False
self.enable_proxy_mocks = False
self.header_path = None
self.include_test_only = False
self.manual_jni_registration = False
self.module_name = ''
self.remove_uncalled_methods = False
self.require_mocks = False
self.__dict__.update(kwargs)
def to_args(self):
ret = super().to_args()
if self.add_stubs_for_missing_native:
ret.append('--add-stubs-for-missing-native')
if self.enable_jni_multiplexing:
ret.append('--enable-jni-multiplexing')
if self.enable_proxy_mocks:
ret.append('--enable-proxy-mocks')
if self.header_path:
ret += ['--header-path', self.header_path]
if self.include_test_only:
ret.append('--include-test-only')
if self.manual_jni_registration:
ret.append('--manual-jni-registration')
if self.module_name:
ret += ['--module-name', self.module_name]
if self.package_prefix:
ret += ['--package_prefix', self.package_prefix]
if self.remove_uncalled_methods:
ret.append('--remove-uncalled-methods')
if self.require_mocks:
ret.append('--require-mocks')
if self.use_proxy_hash:
ret.append('--use-proxy-hash')
return ret
def _MakePrefixes(options):
package_prefix = ''
if options.package_prefix:
package_prefix = options.package_prefix.replace('.', '/') + '/'
module_prefix = ''
if options.module_name:
module_prefix = f'{options.module_name}_'
return package_prefix, module_prefix
class BaseTest(unittest.TestCase):
def _CheckSrcjarGoldens(self, srcjar_path, name_to_goldens):
with zipfile.ZipFile(srcjar_path, 'r') as srcjar:
self.assertEqual(set(srcjar.namelist()), set(name_to_goldens))
for name in srcjar.namelist():
self.assertTrue(
name in name_to_goldens,
f'Found {name} output, but not present in name_to_goldens map.')
contents = srcjar.read(name).decode('utf-8')
self.AssertGoldenTextEquals(contents, name_to_goldens[name])
def _TestEndToEndGeneration(self, input_file, *, srcjar=False, **kwargs):
golden_name = self._testMethodName
options = JniGeneratorOptions(**kwargs)
basename = os.path.splitext(input_file)[0]
header_golden = f'{golden_name}-{basename}_jni.h.golden'
if srcjar:
dir_prefix, file_prefix = _MakePrefixes(options)
name_to_goldens = {
f'{dir_prefix}org/chromium/base/natives/{file_prefix}GEN_JNI.java':
f'{golden_name}-Placeholder-GEN_JNI.java.golden',
f'{dir_prefix}org/chromium/example/jni_generator/{basename}Jni.java':
f'{golden_name}-{basename}Jni.java.golden',
}
with tempfile.TemporaryDirectory() as tdir:
relative_input_file = os.path.join(_JAVA_SRC_DIR, input_file)
if input_file.endswith('.class'):
jar_path = os.path.join(tdir, 'input.jar')
with zipfile.ZipFile(jar_path, 'w') as z:
z.write(relative_input_file, input_file)
options.jar_file = jar_path
options.input_file = input_file
else:
options.input_file = relative_input_file
options.output_dir = tdir
cmd = [os.path.join(_SCRIPT_DIR, 'jni_generator.py')]
if srcjar:
srcjar_path = os.path.join(tdir, 'srcjar.jar')
cmd += ['--srcjar-path', srcjar_path]
cmd += options.to_args()
logging.info('Running: %s', shlex.join(cmd))
subprocess.check_call(cmd)
output_path = os.path.join(tdir, options.output_name)
with open(output_path, 'r') as f:
contents = f.read()
self.AssertGoldenTextEquals(contents, header_golden)
if srcjar:
self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
def _TestEndToEndRegistration(self,
input_files,
src_files_for_asserts_and_stubs=None,
**kwargs):
golden_name = self._testMethodName
options = JniRegistrationGeneratorOptions(**kwargs)
dir_prefix, file_prefix = _MakePrefixes(options)
name_to_goldens = {
f'{dir_prefix}org/chromium/base/natives/{file_prefix}GEN_JNI.java':
f'{golden_name}-Final-GEN_JNI.java.golden',
}
if options.use_proxy_hash:
name_to_goldens[f'{dir_prefix}J/{file_prefix}N.java'] = (
f'{golden_name}-Final-N.java.golden')
header_golden = None
if options.use_proxy_hash or options.manual_jni_registration:
header_golden = f'{golden_name}-Registration.h.golden'
with tempfile.TemporaryDirectory() as tdir:
native_sources = [os.path.join(_JAVA_SRC_DIR, f) for f in input_files]
if src_files_for_asserts_and_stubs:
java_sources = [
os.path.join(_JAVA_SRC_DIR, f)
for f in src_files_for_asserts_and_stubs
]
else:
java_sources = native_sources
cmd = [os.path.join(_SCRIPT_DIR, 'jni_registration_generator.py')]
java_sources_file = pathlib.Path(tdir) / 'java_sources.txt'
java_sources_file.write_text('\n'.join(java_sources))
cmd += ['--java-sources-files', str(java_sources_file)]
if native_sources:
native_sources_file = pathlib.Path(tdir) / 'native_sources.txt'
native_sources_file.write_text('\n'.join(native_sources))
cmd += ['--native-sources-file', str(native_sources_file)]
srcjar_path = os.path.join(tdir, 'srcjar.jar')
cmd += ['--srcjar-path', srcjar_path]
if header_golden:
header_path = os.path.join(tdir, 'header.h')
cmd += ['--header-path', header_path]
cmd += options.to_args()
logging.info('Running: %s', shlex.join(cmd))
subprocess.check_call(cmd)
self._CheckSrcjarGoldens(srcjar_path, name_to_goldens)
if header_golden:
with open(header_path, 'r') as f:
# Temp directory will cause some diffs each time we run if we don't
# normalize.
contents = f.read().replace(
tdir.replace('/', '_').upper(), 'TEMP_DIR')
self.AssertGoldenTextEquals(contents, header_golden)
def _ReadGoldenFile(self, path):
_accessed_goldens.add(path)
if not os.path.exists(path):
return None
with open(path, 'r') as f:
return f.read()
def AssertTextEquals(self, golden_text, generated_text):
if not self.CompareText(golden_text, generated_text):
self.fail('Golden text mismatch.')
def CompareText(self, golden_text, generated_text):
def FilterText(text):
return [
l.strip() for l in text.split('\n')
if not l.startswith('// Copyright')
]
stripped_golden = FilterText(golden_text)
stripped_generated = FilterText(generated_text)
if stripped_golden == stripped_generated:
return True
print(self.id())
for line in difflib.context_diff(stripped_golden, stripped_generated):
print(line)
print('\n\nGenerated')
print('=' * 80)
print(generated_text)
print('=' * 80)
print('Run with:')
print('REBASELINE=1', sys.argv[0])
print('to regenerate the data files.')
def AssertGoldenTextEquals(self, generated_text, golden_file):
"""Compares generated text with the corresponding golden_file
It will instead compare the generated text with
script_dir/golden/golden_file."""
golden_path = os.path.join(_GOLDENS_DIR, golden_file)
golden_text = self._ReadGoldenFile(golden_path)
if _REBASELINE:
if golden_text != generated_text:
print('Updated', golden_path)
with open(golden_path, 'w') as f:
f.write(generated_text)
return
# golden_text is None if no file is found. Better to fail than in
# AssertTextEquals so we can give a clearer message.
if golden_text is None:
self.fail('Golden file does not exist: ' + golden_path)
self.AssertTextEquals(golden_text, generated_text)
@unittest.skipIf(os.name == 'nt', 'Not intended to work on Windows')
class Tests(BaseTest):
def testNonProxy(self):
self._TestEndToEndGeneration('SampleNonProxy.java')
def testBirectionalNonProxy(self):
self._TestEndToEndGeneration('SampleBidirectionalNonProxy.java')
def testBidirectionalClass(self):
self._TestEndToEndGeneration('SampleForTests.java', srcjar=True)
self._TestEndToEndRegistration(['SampleForTests.java'])
def testFromClassFile(self):
self._TestEndToEndGeneration('SampleNonProxy.class')
def testUniqueAnnotations(self):
self._TestEndToEndGeneration('SampleUniqueAnnotations.java', srcjar=True)
def testEndToEndProxyHashed(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
use_proxy_hash=True)
def testEndToEndManualRegistration(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
manual_jni_registration=True)
def testEndToEndProxyJniWithModules(self):
self._TestEndToEndGeneration('SampleModule.java',
srcjar=True,
use_proxy_hash=True,
module_name='module')
self._TestEndToEndRegistration(
['SampleForAnnotationProcessor.java', 'SampleModule.java'],
use_proxy_hash=True,
module_name='module')
def testStubRegistration(self):
input_java_files = ['SampleForAnnotationProcessor.java']
stubs_java_files = input_java_files + [
'TinySample.java', 'SampleProxyEdgeCases.java'
]
extra_input_java_files = ['TinySample2.java']
self._TestEndToEndRegistration(
input_java_files + extra_input_java_files,
src_files_for_asserts_and_stubs=stubs_java_files,
add_stubs_for_missing_native=True,
remove_uncalled_methods=True)
def testFullStubs(self):
self._TestEndToEndRegistration(
[],
src_files_for_asserts_and_stubs=['TinySample.java'],
add_stubs_for_missing_native=True)
def testForTestingKept(self):
input_java_file = 'SampleProxyEdgeCases.java'
self._TestEndToEndGeneration(input_java_file, srcjar=True)
self._TestEndToEndRegistration([input_java_file],
use_proxy_hash=True,
include_test_only=True)
def testForTestingRemoved(self):
self._TestEndToEndRegistration(['SampleProxyEdgeCases.java'],
use_proxy_hash=True,
include_test_only=True)
def testProxyMocks(self):
self._TestEndToEndRegistration(['TinySample.java'], enable_proxy_mocks=True)
def testRequireProxyMocks(self):
self._TestEndToEndRegistration(['TinySample.java'],
enable_proxy_mocks=True,
require_mocks=True)
def testPackagePrefixGenerator(self):
self._TestEndToEndGeneration('SampleForTests.java',
srcjar=True,
package_prefix='this.is.a.package.prefix')
def testPackagePrefixWithManualRegistration(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
package_prefix='this.is.a.package.prefix',
manual_jni_registration=True)
def testPackagePrefixWithProxyHash(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
package_prefix='this.is.a.package.prefix',
use_proxy_hash=True)
def testPackagePrefixWithManualRegistrationWithProxyHash(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
package_prefix='this.is.a.package.prefix',
use_proxy_hash=True,
manual_jni_registration=True)
def testMultiplexing(self):
self._TestEndToEndRegistration(['SampleForAnnotationProcessor.java'],
enable_jni_multiplexing=True,
use_proxy_hash=True)
def main():
try:
unittest.main()
finally:
if _REBASELINE and not any(not x.startswith('-') for x in sys.argv[1:]):
for path in glob.glob(os.path.join(_GOLDENS_DIR, '*.golden')):
if path not in _accessed_goldens:
print('Removing obsolete golden:', path)
os.unlink(path)
if __name__ == '__main__':
main()