• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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