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