blob: 8f41ab027437685fd70fe41d192f98136a36fb5c [file] [log] [blame]
#! -*- 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/nacl_browser/manifest_file/nacl.scons',
'tests/nacl_browser/nameservice/nacl.scons',
'tests/nacl_browser/postmessage_redir/nacl.scons',
'tests/ppapi_browser/bad/nacl.scons',
'tests/ppapi_browser/crash/nacl.scons',
'tests/ppapi_browser/extension_mime_handler/nacl.scons',
'tests/ppapi_browser/manifest/nacl.scons',
'tests/ppapi_browser/ppb_instance/nacl.scons',
'tests/ppapi_browser/ppp_instance/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
#
# Alternatively, we could try to use the chrome commandline switch
# '--disable-extensions-resource-whitelist', but that would not be what
# users will need to do.
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 []
# No browser tests run on arm-thumb2
# Bug http://code.google.com/p/nativeclient/issues/detail?id=2224
if env.Bit('target_arm_thumb2'):
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_arm_thumb2')
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)