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