#! -*- python -*- # Copyright (c) 2012 The Native Client Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import json import os import shutil import sys sys.path.append(Dir('#/tools').abspath) import command_tester import test_lib Import(['pre_base_env']) # Underlay things migrating to ppapi repo. Dir('#/..').addRepository(Dir('#/../ppapi')) # Append a list of files to another, filtering out the files that already exist. # Filtering helps migrate declarations between repos by preventing redundant # declarations from causing an error. def ExtendFileList(existing, additional): # Avoid quadratic behavior by using a set. combined = set() for file_name in existing + additional: if file_name in combined: print 'WARNING: two references to file %s in the build.' % file_name combined.add(file_name) return sorted(combined) ppapi_scons_files = {} ppapi_scons_files['trusted_scons_files'] = [] ppapi_scons_files['untrusted_irt_scons_files'] = [] ppapi_scons_files['nonvariant_test_scons_files'] = [ 'tests/breakpad_crash_test/nacl.scons', 'tests/nacl_browser/browser_dynamic_library/nacl.scons', 'tests/ppapi_test_lib/nacl.scons', ] ppapi_scons_files['irt_variant_test_scons_files'] = [ # 'inbrowser_test_runner' must be in the irt_variant list # otherwise it will run no tests. 'tests/nacl_browser/inbrowser_test_runner/nacl.scons', # Disabled by Brad Chen 4 Sep to try to green Chromium # nacl_integration tests #'tests/nacl_browser/fault_injection/nacl.scons', ] ppapi_scons_files['untrusted_scons_files'] = [ 'src/shared/ppapi/nacl.scons', 'src/untrusted/irt_stub/nacl.scons', 'src/untrusted/nacl_ppapi_util/nacl.scons', ] EXTRA_ENV = [ 'XAUTHORITY', 'HOME', 'DISPLAY', 'SSH_TTY', 'KRB5CCNAME', 'CHROME_DEVEL_SANDBOX' ] def SetupBrowserEnv(env): for var_name in EXTRA_ENV: if var_name in os.environ: env['ENV'][var_name] = os.environ[var_name] pre_base_env.AddMethod(SetupBrowserEnv) def GetHeadlessPrefix(env): if env.Bit('browser_headless') and env.Bit('host_linux'): return ['xvfb-run', '--auto-servernum'] else: # Mac and Windows do not seem to have an equivalent. return [] pre_base_env.AddMethod(GetHeadlessPrefix) # A fake file to depend on if a path to Chrome is not specified. no_browser = pre_base_env.File('chrome_browser_path_not_specified') # SCons attempts to run a test that depends on "no_browser", detect this at # runtime and cause a build error. def NoBrowserError(target, source, env): print target, source, env print ("***\nYou need to specificy chrome_browser_path=... on the " + "command line to run these tests.\n***\n") return 1 pre_base_env.Append(BUILDERS = { 'NoBrowserError': Builder(action=NoBrowserError) }) pre_base_env.NoBrowserError([no_browser], []) def ChromeBinary(env): if 'chrome_browser_path' in ARGUMENTS: return env.File(env.SConstructAbsPath(ARGUMENTS['chrome_browser_path'])) else: return no_browser pre_base_env.AddMethod(ChromeBinary) def GetPPAPIPluginPath(env, allow_64bit_redirect=True): if 'force_ppapi_plugin' in ARGUMENTS: return env.SConstructAbsPath(ARGUMENTS['force_ppapi_plugin']) if env.Bit('mac'): fn = env.File('${STAGING_DIR}/ppNaClPlugin') else: fn = env.File('${STAGING_DIR}/${SHLIBPREFIX}ppNaClPlugin${SHLIBSUFFIX}') if allow_64bit_redirect and env.Bit('target_x86_64'): # On 64-bit Windows and on Mac, we need the 32-bit plugin because # the browser is 32-bit. # Unfortunately it is tricky to build the 32-bit plugin (and all the # libraries it needs) in a 64-bit build... so we'll assume it has already # been built in a previous invocation. # TODO(ncbray) better 32/64 builds. if env.Bit('windows'): fn = env.subst(fn).abspath.replace('-win-x86-64', '-win-x86-32') elif env.Bit('mac'): fn = env.subst(fn).abspath.replace('-mac-x86-64', '-mac-x86-32') return fn pre_base_env.AddMethod(GetPPAPIPluginPath) # runnable-ld.so log has following format: # lib_name => path_to_lib (0x....address) def ParseLibInfoInRunnableLdLog(line): pos = line.find(' => ') if pos < 0: return None lib_name = line[:pos].strip() lib_path = line[pos+4:] pos1 = lib_path.rfind(' (') if pos1 < 0: return None lib_path = lib_path[:pos1] return lib_name, lib_path # Expected name of the temporary .libs file which stores glibc library # dependencies in "lib_name => lib_info" format # (see ParseLibInfoInRunnableLdLog) def GlibcManifestLibsListFilename(manifest_base_name): return '${STAGING_DIR}/%s.libs' % manifest_base_name # Copy libs and manifest to the target directory. # source[0] is a manifest file # source[1] is a .libs file with a list of libs generated by runnable-ld.so def CopyLibsForExtensionCommand(target, source, env): source_manifest = str(source[0]) target_manifest = str(target[0]) shutil.copyfile(source_manifest, target_manifest) target_dir = os.path.dirname(target_manifest) libs_file = open(str(source[1]), 'r') for line in libs_file.readlines(): lib_info = ParseLibInfoInRunnableLdLog(line) if lib_info: lib_name, lib_path = lib_info if lib_path == 'NaClMain': # This is a fake file name, which we cannot copy. continue shutil.copyfile(lib_path, os.path.join(target_dir, lib_name)) shutil.copyfile(env.subst('${NACL_SDK_LIB}/runnable-ld.so'), os.path.join(target_dir, 'runnable-ld.so')) libs_file.close() # Extensions are loaded from directory on disk and so all dynamic libraries # they use must be copied to extension directory. The option --extra_serving_dir # does not help us in this case. def CopyLibsForExtension(env, target_dir, manifest): if not env.Bit('nacl_glibc'): return env.Install(target_dir, manifest) manifest_base_name = os.path.basename(str(env.subst(manifest))) lib_list_node = env.File(GlibcManifestLibsListFilename(manifest_base_name)) nmf_node = env.Command( target_dir + '/' + manifest_base_name, [manifest, lib_list_node], CopyLibsForExtensionCommand) return nmf_node pre_base_env.AddMethod(CopyLibsForExtension) def WhitelistLibsForExtensionCommand(target, source, env): # Load existing extension manifest. src_file = open(source[0].abspath, 'r') src_json = json.load(src_file) src_file.close() # Load existing 'web_accessible_resources' key. if 'web_accessible_resources' not in src_json: src_json['web_accessible_resources'] = [] web_accessible = src_json['web_accessible_resources'] # Load list of libraries, and add libraries to web_accessible list. libs_file = open(source[1].abspath, 'r') for line in libs_file.readlines(): lib_info = ParseLibInfoInRunnableLdLog(line) if lib_info: web_accessible.append(lib_info[0]) # Also add the dynamic loader, which won't be in the libs_file. web_accessible.append('runnable-ld.so') libs_file.close() # Write out the appended-to extension manifest. target_file = open(target[0].abspath, 'w') json.dump(src_json, target_file, sort_keys=True, indent=2) target_file.close() # Whitelist glibc shared libraries (if necessary), so that they are # 'web_accessible_resources'. This allows the libraries hosted at the origin # chrome-extension://[PACKAGE ID]/ # to be made available to webpages that use this NaCl extension, # which are in a different origin. # See: http://code.google.com/chrome/extensions/manifest.html def WhitelistLibsForExtension(env, target_dir, nmf, extension_manifest): if env.Bit('nacl_static_link'): # For static linking, assume the nexe and nmf files are already # whitelisted, so there is no need to add entries to the extension_manifest. return env.Install(target_dir, extension_manifest) nmf_base_name = os.path.basename(env.File(nmf).abspath) lib_list_node = env.File(GlibcManifestLibsListFilename(nmf_base_name)) manifest_base_name = os.path.basename(env.File(extension_manifest).abspath) extension_manifest_node = env.Command( target_dir + '/' + manifest_base_name, [extension_manifest, lib_list_node], WhitelistLibsForExtensionCommand) return extension_manifest_node pre_base_env.AddMethod(WhitelistLibsForExtension) # Generate manifest from newlib manifest and the list of libs generated by # runnable-ld.so. def GenerateManifestFunc(target, source, env): # Open the original manifest and parse it. source_file = open(str(source[0]), 'r') obj = json.load(source_file) source_file.close() # Open the file with ldd-format list of NEEDED libs and parse it. libs_file = open(str(source[1]), 'r') lib_names = [] arch = env.subst('${TARGET_FULLARCH}') for line in libs_file.readlines(): lib_info = ParseLibInfoInRunnableLdLog(line) if lib_info: lib_name, _ = lib_info lib_names.append(lib_name) libs_file.close() # Inject the NEEDED libs into the manifest. if 'files' not in obj: obj['files'] = {} for lib_name in lib_names: obj['files'][lib_name] = {} obj['files'][lib_name][arch] = {} obj['files'][lib_name][arch]['url'] = lib_name # Put what used to be specified under 'program' into 'main.nexe'. obj['files']['main.nexe'] = {} for k, v in obj['program'].items(): obj['files']['main.nexe'][k] = v.copy() v['url'] = 'runnable-ld.so' # Write the new manifest! target_file = open(str(target[0]), 'w') json.dump(obj, target_file, sort_keys=True, indent=2) target_file.close() return 0 def GenerateManifestDynamicLink(env, dest_file, lib_list_file, manifest, exe_file): # Run sel_ldr on the nexe to trace the NEEDED libraries. lib_list_node = env.Command( lib_list_file, [env.GetSelLdr(), '${NACL_SDK_LIB}/runnable-ld.so', exe_file, '${SCONSTRUCT_DIR}/DEPS'], # We ignore the return code using '-' in order to build tests # where binaries do not validate. This is a Scons feature. '-${SOURCES[0]} -a -E LD_TRACE_LOADED_OBJECTS=1 ${SOURCES[1]} ' '--library-path ${NACL_SDK_LIB}:${LIB_DIR} ${SOURCES[2].posix} ' '> ${TARGET}') return env.Command(dest_file, [manifest, lib_list_node], GenerateManifestFunc)[0] def GenerateSimpleManifestStaticLink(env, dest_file, exe_name): def Func(target, source, env): archs = ('x86-32', 'x86-64', 'arm') nmf_data = {'program': dict((arch, {'url': '%s_%s.nexe' % (exe_name, arch)}) for arch in archs)} fh = open(target[0].abspath, 'w') json.dump(nmf_data, fh, sort_keys=True, indent=2) fh.close() node = env.Command(dest_file, [], Func)[0] # Scons does not track the dependency of dest_file on exe_name or on # the Python code above, so we should always recreate dest_file when # it is used. env.AlwaysBuild(node) return node def GenerateSimpleManifest(env, dest_file, exe_name): if env.Bit('nacl_static_link'): return GenerateSimpleManifestStaticLink(env, dest_file, exe_name) else: static_manifest = GenerateSimpleManifestStaticLink( env, '%s.static' % dest_file, exe_name) return GenerateManifestDynamicLink( env, dest_file, '%s.tmp_lib_list' % dest_file, static_manifest, '${STAGING_DIR}/%s.nexe' % env.ProgramNameForNmf(exe_name)) pre_base_env.AddMethod(GenerateSimpleManifest) # Returns a pair (main program, is_portable), based on the program # specified in manifest file. def GetMainProgramFromManifest(env, manifest): obj = json.loads(env.File(manifest).get_contents()) program_dict = obj['program'] return program_dict[env.subst('${TARGET_FULLARCH}')]['url'] # Returns scons node for generated manifest. def GeneratedManifestNode(env, manifest): manifest = env.subst(manifest) manifest_base_name = os.path.basename(manifest) main_program = GetMainProgramFromManifest(env, manifest) result = env.File('${STAGING_DIR}/' + manifest_base_name) # Always generate the manifest for nacl_glibc. # For nacl_glibc, generating the mapping of shared libraries is non-trivial. if not env.Bit('nacl_glibc'): env.Install('${STAGING_DIR}', manifest) return result return GenerateManifestDynamicLink( env, '${STAGING_DIR}/' + manifest_base_name, # Note that CopyLibsForExtension() and WhitelistLibsForExtension() # assume that it can find the library list file under this filename. GlibcManifestLibsListFilename(manifest_base_name), manifest, env.File('${STAGING_DIR}/' + os.path.basename(main_program))) return result # Compares output_file and golden_file. # If they are different, prints the difference and returns 1. # Otherwise, returns 0. def CheckGoldenFile(golden_file, output_file, filter_regex, filter_inverse, filter_group_only): golden = open(golden_file).read() actual = open(output_file).read() if filter_regex is not None: actual = test_lib.RegexpFilterLines( filter_regex, filter_inverse, filter_group_only, actual) if command_tester.DifferentFromGolden(actual, golden, output_file): return 1 return 0 # Returns action that compares output_file and golden_file. # This action can be attached to the node with # env.AddPostAction(target, action) def GoldenFileCheckAction(env, output_file, golden_file, filter_regex=None, filter_inverse=False, filter_group_only=False): def ActionFunc(target, source, env): return CheckGoldenFile(env.subst(golden_file), env.subst(output_file), filter_regex, filter_inverse, filter_group_only) return env.Action(ActionFunc) def PPAPIBrowserTester(env, target, url, files, nmfs=None, # List of executable basenames to generate # manifest files for. nmf_names=(), map_files=(), extensions=(), mime_types=(), timeout=30, log_verbosity=2, args=[], # list of key/value pairs that are passed to the test test_args=(), # list of "--flag=value" pairs (no spaces!) browser_flags=None, # redirect streams of NaCl program to files nacl_exe_stdin=None, nacl_exe_stdout=None, nacl_exe_stderr=None, python_tester_script=None, **extra): if 'TRUSTED_ENV' not in env: return [] # Handle issues with mutating any python default arg lists. if browser_flags is None: browser_flags = [] # Lint the extra arguments that are being passed to the tester. special_args = ['--ppapi_plugin', '--sel_ldr', '--irt_library', '--file', '--map_file', '--extension', '--mime_type', '--tool', '--browser_flag', '--test_arg'] for arg_name in special_args: if arg_name in args: raise Exception('%s: %r is a test argument provided by the SCons test' ' wrapper, do not specify it as an additional argument' % (target, arg_name)) env = env.Clone() env.SetupBrowserEnv() if 'scale_timeout' in ARGUMENTS: timeout = timeout * int(ARGUMENTS['scale_timeout']) if python_tester_script is None: python_tester_script = env.File('${SCONSTRUCT_DIR}/tools/browser_tester' '/browser_tester.py') command = env.GetHeadlessPrefix() + [ '${PYTHON}', python_tester_script, '--browser_path', env.ChromeBinary(), '--url', url, # Fail if there is no response for X seconds. '--timeout', str(timeout)] for dep_file in files: command.extend(['--file', dep_file]) for extension in extensions: command.extend(['--extension', extension]) for dest_path, dep_file in map_files: command.extend(['--map_file', dest_path, dep_file]) for file_ext, mime_type in mime_types: command.extend(['--mime_type', file_ext, mime_type]) command.extend(['--serving_dir', '${NACL_SDK_LIB}']) command.extend(['--serving_dir', '${LIB_DIR}']) if 'browser_tester_bw' in ARGUMENTS: command.extend(['-b', ARGUMENTS['browser_tester_bw']]) if not nmfs is None: for nmf_file in nmfs: generated_manifest = GeneratedManifestNode(env, nmf_file) # We need to add generated manifests to the list of default targets. # The manifests should be generated even if the tests are not run - # the manifests may be needed for manual testing. for group in env['COMPONENT_TEST_PROGRAM_GROUPS']: env.Alias(group, generated_manifest) # Generated manifests are served in the root of the HTTP server command.extend(['--file', generated_manifest]) for nmf_name in nmf_names: tmp_manifest = '%s.tmp/%s.nmf' % (target, nmf_name) command.extend(['--map_file', '%s.nmf' % nmf_name, env.GenerateSimpleManifest(tmp_manifest, nmf_name)]) if 'browser_test_tool' in ARGUMENTS: command.extend(['--tool', ARGUMENTS['browser_test_tool']]) # Suppress debugging information on the Chrome waterfall. if env.Bit('disable_flaky_tests') and '--debug' in args: args.remove('--debug') command.extend(args) for flag in browser_flags: if flag.find(' ') != -1: raise Exception('Spaces not allowed in browser_flags: ' 'use --flag=value instead') command.extend(['--browser_flag', flag]) for key, value in test_args: command.extend(['--test_arg', str(key), str(value)]) # Set a given file to be the nexe's stdin. if nacl_exe_stdin is not None: command.extend(['--nacl_exe_stdin', env.subst(nacl_exe_stdin['file'])]) post_actions = [] side_effects = [] # Set a given file to be the nexe's stdout or stderr. The tester also # compares this output against a golden file. for stream, params in ( ('stdout', nacl_exe_stdout), ('stderr', nacl_exe_stderr)): if params is None: continue stream_file = env.subst(params['file']) side_effects.append(stream_file) command.extend(['--nacl_exe_' + stream, stream_file]) if 'golden' in params: golden_file = env.subst(params['golden']) filter_regex = params.get('filter_regex', None) filter_inverse = params.get('filter_inverse', False) filter_group_only = params.get('filter_group_only', False) post_actions.append( GoldenFileCheckAction( env, stream_file, golden_file, filter_regex, filter_inverse, filter_group_only)) if env.ShouldUseVerboseOptions(extra): env.MakeVerboseExtraOptions(target, log_verbosity, extra) # Heuristic for when to capture output... capture_output = (extra.pop('capture_output', False) or 'process_output_single' in extra) node = env.CommandTest(target, command, # Set to 'huge' so that the browser tester's timeout # takes precedence over the default of the test_suite. size='huge', capture_output=capture_output, **extra) for side_effect in side_effects: env.SideEffect(side_effect, node) # We can't check output if the test is not run. if not env.Bit('do_not_run_tests'): for action in post_actions: env.AddPostAction(node, action) return node pre_base_env.AddMethod(PPAPIBrowserTester) # Disabled for ARM and MIPS because Chrome binaries for ARM and MIPS are not # available. def PPAPIBrowserTesterIsBroken(env): return env.Bit('target_arm') or env.Bit('target_mips32') pre_base_env.AddMethod(PPAPIBrowserTesterIsBroken) # 3D is disabled everywhere def PPAPIGraphics3DIsBroken(env): return True pre_base_env.AddMethod(PPAPIGraphics3DIsBroken) def AddChromeFilesFromGroup(env, file_group): env['BUILD_SCONSCRIPTS'] = ExtendFileList( env.get('BUILD_SCONSCRIPTS', []), ppapi_scons_files[file_group]) pre_base_env.AddMethod(AddChromeFilesFromGroup)