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