1#! -*- python -*- 2# Copyright (c) 2012 The Native Client Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import json 7import os 8import shutil 9import sys 10 11sys.path.append(Dir('#/tools').abspath) 12import command_tester 13import test_lib 14 15Import(['pre_base_env']) 16 17# Underlay things migrating to ppapi repo. 18Dir('#/..').addRepository(Dir('#/../ppapi')) 19 20# Append a list of files to another, filtering out the files that already exist. 21# Filtering helps migrate declarations between repos by preventing redundant 22# declarations from causing an error. 23def ExtendFileList(existing, additional): 24 # Avoid quadratic behavior by using a set. 25 combined = set() 26 for file_name in existing + additional: 27 if file_name in combined: 28 print 'WARNING: two references to file %s in the build.' % file_name 29 combined.add(file_name) 30 return sorted(combined) 31 32 33ppapi_scons_files = {} 34ppapi_scons_files['trusted_scons_files'] = [] 35ppapi_scons_files['untrusted_irt_scons_files'] = [] 36 37ppapi_scons_files['nonvariant_test_scons_files'] = [ 38 'tests/breakpad_crash_test/nacl.scons', 39 'tests/nacl_browser/browser_dynamic_library/nacl.scons', 40 'tests/nacl_browser/manifest_file/nacl.scons', 41 'tests/nacl_browser/nameservice/nacl.scons', 42 'tests/ppapi_browser/bad/nacl.scons', 43 'tests/ppapi_browser/extension_mime_handler/nacl.scons', 44 'tests/ppapi_browser/manifest/nacl.scons', 45 'tests/ppapi_test_lib/nacl.scons', 46] 47 48ppapi_scons_files['irt_variant_test_scons_files'] = [ 49 # 'inbrowser_test_runner' must be in the irt_variant list 50 # otherwise it will run no tests. 51 'tests/nacl_browser/inbrowser_test_runner/nacl.scons', 52 # Disabled by Brad Chen 4 Sep to try to green Chromium 53 # nacl_integration tests 54 #'tests/nacl_browser/fault_injection/nacl.scons', 55] 56 57ppapi_scons_files['untrusted_scons_files'] = [ 58 'src/shared/ppapi/nacl.scons', 59 'src/untrusted/irt_stub/nacl.scons', 60 'src/untrusted/nacl_ppapi_util/nacl.scons', 61] 62 63 64EXTRA_ENV = [ 65 'XAUTHORITY', 'HOME', 'DISPLAY', 'SSH_TTY', 'KRB5CCNAME', 66 'CHROME_DEVEL_SANDBOX' ] 67 68def SetupBrowserEnv(env): 69 for var_name in EXTRA_ENV: 70 if var_name in os.environ: 71 env['ENV'][var_name] = os.environ[var_name] 72 73pre_base_env.AddMethod(SetupBrowserEnv) 74 75 76def GetHeadlessPrefix(env): 77 if env.Bit('browser_headless') and env.Bit('host_linux'): 78 return ['xvfb-run', '--auto-servernum'] 79 else: 80 # Mac and Windows do not seem to have an equivalent. 81 return [] 82 83pre_base_env.AddMethod(GetHeadlessPrefix) 84 85 86# A fake file to depend on if a path to Chrome is not specified. 87no_browser = pre_base_env.File('chrome_browser_path_not_specified') 88 89 90# SCons attempts to run a test that depends on "no_browser", detect this at 91# runtime and cause a build error. 92def NoBrowserError(target, source, env): 93 print target, source, env 94 print ("***\nYou need to specificy chrome_browser_path=... on the " + 95 "command line to run these tests.\n***\n") 96 return 1 97 98pre_base_env.Append(BUILDERS = { 99 'NoBrowserError': Builder(action=NoBrowserError) 100}) 101 102pre_base_env.NoBrowserError([no_browser], []) 103 104 105def ChromeBinary(env): 106 if 'chrome_browser_path' in ARGUMENTS: 107 return env.File(env.SConstructAbsPath(ARGUMENTS['chrome_browser_path'])) 108 else: 109 return no_browser 110 111pre_base_env.AddMethod(ChromeBinary) 112 113 114def GetPPAPIPluginPath(env, allow_64bit_redirect=True): 115 if 'force_ppapi_plugin' in ARGUMENTS: 116 return env.SConstructAbsPath(ARGUMENTS['force_ppapi_plugin']) 117 if env.Bit('mac'): 118 fn = env.File('${STAGING_DIR}/ppNaClPlugin') 119 else: 120 fn = env.File('${STAGING_DIR}/${SHLIBPREFIX}ppNaClPlugin${SHLIBSUFFIX}') 121 if allow_64bit_redirect and env.Bit('target_x86_64'): 122 # On 64-bit Windows and on Mac, we need the 32-bit plugin because 123 # the browser is 32-bit. 124 # Unfortunately it is tricky to build the 32-bit plugin (and all the 125 # libraries it needs) in a 64-bit build... so we'll assume it has already 126 # been built in a previous invocation. 127 # TODO(ncbray) better 32/64 builds. 128 if env.Bit('windows'): 129 fn = env.subst(fn).abspath.replace('-win-x86-64', '-win-x86-32') 130 elif env.Bit('mac'): 131 fn = env.subst(fn).abspath.replace('-mac-x86-64', '-mac-x86-32') 132 return fn 133 134pre_base_env.AddMethod(GetPPAPIPluginPath) 135 136 137# runnable-ld.so log has following format: 138# lib_name => path_to_lib (0x....address) 139def ParseLibInfoInRunnableLdLog(line): 140 pos = line.find(' => ') 141 if pos < 0: 142 return None 143 lib_name = line[:pos].strip() 144 lib_path = line[pos+4:] 145 pos1 = lib_path.rfind(' (') 146 if pos1 < 0: 147 return None 148 lib_path = lib_path[:pos1] 149 return lib_name, lib_path 150 151 152# Expected name of the temporary .libs file which stores glibc library 153# dependencies in "lib_name => lib_info" format 154# (see ParseLibInfoInRunnableLdLog) 155def GlibcManifestLibsListFilename(manifest_base_name): 156 return '${STAGING_DIR}/%s.libs' % manifest_base_name 157 158 159# Copy libs and manifest to the target directory. 160# source[0] is a manifest file 161# source[1] is a .libs file with a list of libs generated by runnable-ld.so 162def CopyLibsForExtensionCommand(target, source, env): 163 source_manifest = str(source[0]) 164 target_manifest = str(target[0]) 165 shutil.copyfile(source_manifest, target_manifest) 166 target_dir = os.path.dirname(target_manifest) 167 libs_file = open(str(source[1]), 'r') 168 for line in libs_file.readlines(): 169 lib_info = ParseLibInfoInRunnableLdLog(line) 170 if lib_info: 171 lib_name, lib_path = lib_info 172 if lib_path == 'NaClMain': 173 # This is a fake file name, which we cannot copy. 174 continue 175 shutil.copyfile(lib_path, os.path.join(target_dir, lib_name)) 176 shutil.copyfile(env.subst('${NACL_SDK_LIB}/runnable-ld.so'), 177 os.path.join(target_dir, 'runnable-ld.so')) 178 libs_file.close() 179 180 181# Extensions are loaded from directory on disk and so all dynamic libraries 182# they use must be copied to extension directory. The option --extra_serving_dir 183# does not help us in this case. 184def CopyLibsForExtension(env, target_dir, manifest): 185 if not env.Bit('nacl_glibc'): 186 return env.Install(target_dir, manifest) 187 manifest_base_name = os.path.basename(str(env.subst(manifest))) 188 lib_list_node = env.File(GlibcManifestLibsListFilename(manifest_base_name)) 189 nmf_node = env.Command( 190 target_dir + '/' + manifest_base_name, 191 [manifest, lib_list_node], 192 CopyLibsForExtensionCommand) 193 return nmf_node 194 195pre_base_env.AddMethod(CopyLibsForExtension) 196 197 198 199def WhitelistLibsForExtensionCommand(target, source, env): 200 # Load existing extension manifest. 201 src_file = open(source[0].abspath, 'r') 202 src_json = json.load(src_file) 203 src_file.close() 204 205 # Load existing 'web_accessible_resources' key. 206 if 'web_accessible_resources' not in src_json: 207 src_json['web_accessible_resources'] = [] 208 web_accessible = src_json['web_accessible_resources'] 209 210 # Load list of libraries, and add libraries to web_accessible list. 211 libs_file = open(source[1].abspath, 'r') 212 for line in libs_file.readlines(): 213 lib_info = ParseLibInfoInRunnableLdLog(line) 214 if lib_info: 215 web_accessible.append(lib_info[0]) 216 # Also add the dynamic loader, which won't be in the libs_file. 217 web_accessible.append('runnable-ld.so') 218 libs_file.close() 219 220 # Write out the appended-to extension manifest. 221 target_file = open(target[0].abspath, 'w') 222 json.dump(src_json, target_file, sort_keys=True, indent=2) 223 target_file.close() 224 225 226# Whitelist glibc shared libraries (if necessary), so that they are 227# 'web_accessible_resources'. This allows the libraries hosted at the origin 228# chrome-extension://[PACKAGE ID]/ 229# to be made available to webpages that use this NaCl extension, 230# which are in a different origin. 231# See: http://code.google.com/chrome/extensions/manifest.html 232# 233# Alternatively, we could try to use the chrome commandline switch 234# '--disable-extensions-resource-whitelist', but that would not be what 235# users will need to do. 236def WhitelistLibsForExtension(env, target_dir, nmf, extension_manifest): 237 if env.Bit('nacl_static_link'): 238 # For static linking, assume the nexe and nmf files are already 239 # whitelisted, so there is no need to add entries to the extension_manifest. 240 return env.Install(target_dir, extension_manifest) 241 nmf_base_name = os.path.basename(env.File(nmf).abspath) 242 lib_list_node = env.File(GlibcManifestLibsListFilename(nmf_base_name)) 243 manifest_base_name = os.path.basename(env.File(extension_manifest).abspath) 244 extension_manifest_node = env.Command( 245 target_dir + '/' + manifest_base_name, 246 [extension_manifest, lib_list_node], 247 WhitelistLibsForExtensionCommand) 248 return extension_manifest_node 249 250pre_base_env.AddMethod(WhitelistLibsForExtension) 251 252 253# Generate manifest from newlib manifest and the list of libs generated by 254# runnable-ld.so. 255def GenerateManifestFunc(target, source, env): 256 # Open the original manifest and parse it. 257 source_file = open(str(source[0]), 'r') 258 obj = json.load(source_file) 259 source_file.close() 260 # Open the file with ldd-format list of NEEDED libs and parse it. 261 libs_file = open(str(source[1]), 'r') 262 lib_names = [] 263 arch = env.subst('${TARGET_FULLARCH}') 264 for line in libs_file.readlines(): 265 lib_info = ParseLibInfoInRunnableLdLog(line) 266 if lib_info: 267 lib_name, _ = lib_info 268 lib_names.append(lib_name) 269 libs_file.close() 270 # Inject the NEEDED libs into the manifest. 271 if 'files' not in obj: 272 obj['files'] = {} 273 for lib_name in lib_names: 274 obj['files'][lib_name] = {} 275 obj['files'][lib_name][arch] = {} 276 obj['files'][lib_name][arch]['url'] = lib_name 277 # Put what used to be specified under 'program' into 'main.nexe'. 278 obj['files']['main.nexe'] = {} 279 for k, v in obj['program'].items(): 280 obj['files']['main.nexe'][k] = v.copy() 281 v['url'] = 'runnable-ld.so' 282 # Write the new manifest! 283 target_file = open(str(target[0]), 'w') 284 json.dump(obj, target_file, sort_keys=True, indent=2) 285 target_file.close() 286 return 0 287 288 289def GenerateManifestDynamicLink(env, dest_file, lib_list_file, 290 manifest, exe_file): 291 # Run sel_ldr on the nexe to trace the NEEDED libraries. 292 lib_list_node = env.Command( 293 lib_list_file, 294 [env.GetSelLdr(), 295 '${NACL_SDK_LIB}/runnable-ld.so', 296 exe_file, 297 '${SCONSTRUCT_DIR}/DEPS'], 298 # We ignore the return code using '-' in order to build tests 299 # where binaries do not validate. This is a Scons feature. 300 '-${SOURCES[0]} -a -E LD_TRACE_LOADED_OBJECTS=1 ${SOURCES[1]} ' 301 '--library-path ${NACL_SDK_LIB}:${LIB_DIR} ${SOURCES[2].posix} ' 302 '> ${TARGET}') 303 return env.Command(dest_file, 304 [manifest, lib_list_node], 305 GenerateManifestFunc)[0] 306 307 308def GenerateSimpleManifestStaticLink(env, dest_file, exe_name): 309 def Func(target, source, env): 310 archs = ('x86-32', 'x86-64', 'arm') 311 nmf_data = {'program': dict((arch, {'url': '%s_%s.nexe' % (exe_name, arch)}) 312 for arch in archs)} 313 fh = open(target[0].abspath, 'w') 314 json.dump(nmf_data, fh, sort_keys=True, indent=2) 315 fh.close() 316 node = env.Command(dest_file, [], Func)[0] 317 # Scons does not track the dependency of dest_file on exe_name or on 318 # the Python code above, so we should always recreate dest_file when 319 # it is used. 320 env.AlwaysBuild(node) 321 return node 322 323 324def GenerateSimpleManifest(env, dest_file, exe_name): 325 if env.Bit('nacl_static_link'): 326 return GenerateSimpleManifestStaticLink(env, dest_file, exe_name) 327 else: 328 static_manifest = GenerateSimpleManifestStaticLink( 329 env, '%s.static' % dest_file, exe_name) 330 return GenerateManifestDynamicLink( 331 env, dest_file, '%s.tmp_lib_list' % dest_file, static_manifest, 332 '${STAGING_DIR}/%s.nexe' % env.ProgramNameForNmf(exe_name)) 333 334pre_base_env.AddMethod(GenerateSimpleManifest) 335 336 337# Returns a pair (main program, is_portable), based on the program 338# specified in manifest file. 339def GetMainProgramFromManifest(env, manifest): 340 obj = json.loads(env.File(manifest).get_contents()) 341 program_dict = obj['program'] 342 return program_dict[env.subst('${TARGET_FULLARCH}')]['url'] 343 344 345# Returns scons node for generated manifest. 346def GeneratedManifestNode(env, manifest): 347 manifest = env.subst(manifest) 348 manifest_base_name = os.path.basename(manifest) 349 main_program = GetMainProgramFromManifest(env, manifest) 350 result = env.File('${STAGING_DIR}/' + manifest_base_name) 351 # Always generate the manifest for nacl_glibc. 352 # For nacl_glibc, generating the mapping of shared libraries is non-trivial. 353 if not env.Bit('nacl_glibc'): 354 env.Install('${STAGING_DIR}', manifest) 355 return result 356 return GenerateManifestDynamicLink( 357 env, '${STAGING_DIR}/' + manifest_base_name, 358 # Note that CopyLibsForExtension() and WhitelistLibsForExtension() 359 # assume that it can find the library list file under this filename. 360 GlibcManifestLibsListFilename(manifest_base_name), 361 manifest, 362 env.File('${STAGING_DIR}/' + os.path.basename(main_program))) 363 return result 364 365 366# Compares output_file and golden_file. 367# If they are different, prints the difference and returns 1. 368# Otherwise, returns 0. 369def CheckGoldenFile(golden_file, output_file, 370 filter_regex, filter_inverse, filter_group_only): 371 golden = open(golden_file).read() 372 actual = open(output_file).read() 373 if filter_regex is not None: 374 actual = test_lib.RegexpFilterLines( 375 filter_regex, 376 filter_inverse, 377 filter_group_only, 378 actual) 379 if command_tester.DifferentFromGolden(actual, golden, output_file): 380 return 1 381 return 0 382 383 384# Returns action that compares output_file and golden_file. 385# This action can be attached to the node with 386# env.AddPostAction(target, action) 387def GoldenFileCheckAction(env, output_file, golden_file, 388 filter_regex=None, filter_inverse=False, 389 filter_group_only=False): 390 def ActionFunc(target, source, env): 391 return CheckGoldenFile(env.subst(golden_file), env.subst(output_file), 392 filter_regex, filter_inverse, filter_group_only) 393 394 return env.Action(ActionFunc) 395 396 397def PPAPIBrowserTester(env, 398 target, 399 url, 400 files, 401 nmfs=None, 402 # List of executable basenames to generate 403 # manifest files for. 404 nmf_names=(), 405 map_files=(), 406 extensions=(), 407 mime_types=(), 408 timeout=30, 409 log_verbosity=2, 410 args=[], 411 # list of key/value pairs that are passed to the test 412 test_args=(), 413 # list of "--flag=value" pairs (no spaces!) 414 browser_flags=None, 415 # redirect streams of NaCl program to files 416 nacl_exe_stdin=None, 417 nacl_exe_stdout=None, 418 nacl_exe_stderr=None, 419 python_tester_script=None, 420 **extra): 421 if 'TRUSTED_ENV' not in env: 422 return [] 423 424 # No browser tests run on arm-thumb2 425 # Bug http://code.google.com/p/nativeclient/issues/detail?id=2224 426 if env.Bit('target_arm_thumb2'): 427 return [] 428 429 # Handle issues with mutating any python default arg lists. 430 if browser_flags is None: 431 browser_flags = [] 432 433 # Lint the extra arguments that are being passed to the tester. 434 special_args = ['--ppapi_plugin', '--sel_ldr', '--irt_library', '--file', 435 '--map_file', '--extension', '--mime_type', '--tool', 436 '--browser_flag', '--test_arg'] 437 for arg_name in special_args: 438 if arg_name in args: 439 raise Exception('%s: %r is a test argument provided by the SCons test' 440 ' wrapper, do not specify it as an additional argument' % 441 (target, arg_name)) 442 443 env = env.Clone() 444 env.SetupBrowserEnv() 445 446 if 'scale_timeout' in ARGUMENTS: 447 timeout = timeout * int(ARGUMENTS['scale_timeout']) 448 449 if python_tester_script is None: 450 python_tester_script = env.File('${SCONSTRUCT_DIR}/tools/browser_tester' 451 '/browser_tester.py') 452 command = env.GetHeadlessPrefix() + [ 453 '${PYTHON}', python_tester_script, 454 '--browser_path', env.ChromeBinary(), 455 '--url', url, 456 # Fail if there is no response for X seconds. 457 '--timeout', str(timeout)] 458 for dep_file in files: 459 command.extend(['--file', dep_file]) 460 for extension in extensions: 461 command.extend(['--extension', extension]) 462 for dest_path, dep_file in map_files: 463 command.extend(['--map_file', dest_path, dep_file]) 464 for file_ext, mime_type in mime_types: 465 command.extend(['--mime_type', file_ext, mime_type]) 466 command.extend(['--serving_dir', '${NACL_SDK_LIB}']) 467 command.extend(['--serving_dir', '${LIB_DIR}']) 468 if 'browser_tester_bw' in ARGUMENTS: 469 command.extend(['-b', ARGUMENTS['browser_tester_bw']]) 470 if not nmfs is None: 471 for nmf_file in nmfs: 472 generated_manifest = GeneratedManifestNode(env, nmf_file) 473 # We need to add generated manifests to the list of default targets. 474 # The manifests should be generated even if the tests are not run - 475 # the manifests may be needed for manual testing. 476 for group in env['COMPONENT_TEST_PROGRAM_GROUPS']: 477 env.Alias(group, generated_manifest) 478 # Generated manifests are served in the root of the HTTP server 479 command.extend(['--file', generated_manifest]) 480 for nmf_name in nmf_names: 481 tmp_manifest = '%s.tmp/%s.nmf' % (target, nmf_name) 482 command.extend(['--map_file', '%s.nmf' % nmf_name, 483 env.GenerateSimpleManifest(tmp_manifest, nmf_name)]) 484 if 'browser_test_tool' in ARGUMENTS: 485 command.extend(['--tool', ARGUMENTS['browser_test_tool']]) 486 487 # Suppress debugging information on the Chrome waterfall. 488 if env.Bit('disable_flaky_tests') and '--debug' in args: 489 args.remove('--debug') 490 491 command.extend(args) 492 for flag in browser_flags: 493 if flag.find(' ') != -1: 494 raise Exception('Spaces not allowed in browser_flags: ' 495 'use --flag=value instead') 496 command.extend(['--browser_flag', flag]) 497 for key, value in test_args: 498 command.extend(['--test_arg', str(key), str(value)]) 499 500 # Set a given file to be the nexe's stdin. 501 if nacl_exe_stdin is not None: 502 command.extend(['--nacl_exe_stdin', env.subst(nacl_exe_stdin['file'])]) 503 504 post_actions = [] 505 side_effects = [] 506 # Set a given file to be the nexe's stdout or stderr. The tester also 507 # compares this output against a golden file. 508 for stream, params in ( 509 ('stdout', nacl_exe_stdout), 510 ('stderr', nacl_exe_stderr)): 511 if params is None: 512 continue 513 stream_file = env.subst(params['file']) 514 side_effects.append(stream_file) 515 command.extend(['--nacl_exe_' + stream, stream_file]) 516 if 'golden' in params: 517 golden_file = env.subst(params['golden']) 518 filter_regex = params.get('filter_regex', None) 519 filter_inverse = params.get('filter_inverse', False) 520 filter_group_only = params.get('filter_group_only', False) 521 post_actions.append( 522 GoldenFileCheckAction( 523 env, stream_file, golden_file, 524 filter_regex, filter_inverse, filter_group_only)) 525 526 if env.ShouldUseVerboseOptions(extra): 527 env.MakeVerboseExtraOptions(target, log_verbosity, extra) 528 # Heuristic for when to capture output... 529 capture_output = (extra.pop('capture_output', False) 530 or 'process_output_single' in extra) 531 node = env.CommandTest(target, 532 command, 533 # Set to 'huge' so that the browser tester's timeout 534 # takes precedence over the default of the test_suite. 535 size='huge', 536 capture_output=capture_output, 537 **extra) 538 for side_effect in side_effects: 539 env.SideEffect(side_effect, node) 540 # We can't check output if the test is not run. 541 if not env.Bit('do_not_run_tests'): 542 for action in post_actions: 543 env.AddPostAction(node, action) 544 return node 545 546pre_base_env.AddMethod(PPAPIBrowserTester) 547 548 549# Disabled for ARM and MIPS because Chrome binaries for ARM and MIPS are not 550# available. 551def PPAPIBrowserTesterIsBroken(env): 552 return (env.Bit('target_arm') or env.Bit('target_arm_thumb2') 553 or env.Bit('target_mips32')) 554 555pre_base_env.AddMethod(PPAPIBrowserTesterIsBroken) 556 557# 3D is disabled everywhere 558def PPAPIGraphics3DIsBroken(env): 559 return True 560 561pre_base_env.AddMethod(PPAPIGraphics3DIsBroken) 562 563 564def AddChromeFilesFromGroup(env, file_group): 565 env['BUILD_SCONSCRIPTS'] = ExtendFileList( 566 env.get('BUILD_SCONSCRIPTS', []), 567 ppapi_scons_files[file_group]) 568 569pre_base_env.AddMethod(AddChromeFilesFromGroup) 570