| #! -*- 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', |
| ] |
| |
| 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', |
| ] |
| |
| ppapi_scons_files['untrusted_scons_files'] = [ |
| 'src/untrusted/irt_stub/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) |
| |
| |
| # 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('build_arm') or env.Bit('build_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) |