blob: 3543a7669dbd6953eb12c328c22bd4affa5b67d2 [file] [log] [blame]
# Library for JSTest manifests.
#
# This includes classes for representing and parsing JS manifests.
from __future__ import print_function
import os, re, sys
from subprocess import Popen, PIPE
from tests import RefTestCase
def split_path_into_dirs(path):
dirs = [path]
while True:
path, tail = os.path.split(path)
if not tail:
break
dirs.append(path)
return dirs
class XULInfo:
def __init__(self, abi, os, isdebug):
self.abi = abi
self.os = os
self.isdebug = isdebug
self.browserIsRemote = False
def as_js(self):
"""Return JS that when executed sets up variables so that JS expression
predicates on XUL build info evaluate properly."""
return ('var xulRuntime = {{ OS: "{}", XPCOMABI: "{}", shell: true }};'
'var isDebugBuild={}; var Android={}; '
'var browserIsRemote={}'.format(
self.os,
self.abi,
str(self.isdebug).lower(),
str(self.os == "Android").lower(),
str(self.browserIsRemote).lower()))
@classmethod
def create(cls, jsdir):
"""Create a XULInfo based on the current platform's characteristics."""
# Our strategy is to find the autoconf.mk generated for the build and
# read the values from there.
# Find config/autoconf.mk.
dirs = split_path_into_dirs(os.getcwd()) + split_path_into_dirs(jsdir)
path = None
for dir in dirs:
_path = os.path.join(dir, 'config/autoconf.mk')
if os.path.isfile(_path):
path = _path
break
if path == None:
print("Can't find config/autoconf.mk on a directory containing"
" the JS shell (searched from {})".format(jsdir))
sys.exit(1)
# Read the values.
val_re = re.compile(r'(TARGET_XPCOM_ABI|OS_TARGET|MOZ_DEBUG)\s*=\s*(.*)')
kw = {'isdebug': False}
for line in open(path):
m = val_re.match(line)
if m:
key, val = m.groups()
val = val.rstrip()
if key == 'TARGET_XPCOM_ABI':
kw['abi'] = val
if key == 'OS_TARGET':
kw['os'] = val
if key == 'MOZ_DEBUG':
kw['isdebug'] = (val == '1')
return cls(**kw)
class XULInfoTester:
def __init__(self, xulinfo, js_bin):
self.js_prologue = xulinfo.as_js()
self.js_bin = js_bin
# Maps JS expr to evaluation result.
self.cache = {}
def test(self, cond):
"""Test a XUL predicate condition against this local info."""
ans = self.cache.get(cond, None)
if ans is None:
cmd = [
self.js_bin,
'-e', self.js_prologue,
'-e', 'print(!!({}))'.format(cond)
]
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
out, err = p.communicate()
if out in ('true\n', 'true\r\n'):
ans = True
elif out in ('false\n', 'false\r\n'):
ans = False
else:
raise Exception("Failed to test XUL condition {!r};"
" output was {!r}, stderr was {!r}".format(
cond, out, err))
self.cache[cond] = ans
return ans
class NullXULInfoTester:
"""Can be used to parse manifests without a JS shell."""
def test(self, cond):
return False
def _parse_one(testcase, xul_tester):
pos = 0
parts = testcase.terms.split()
while pos < len(parts):
if parts[pos] == 'fails':
testcase.expect = False
pos += 1
elif parts[pos] == 'skip':
testcase.expect = testcase.enable = False
pos += 1
elif parts[pos] == 'random':
testcase.random = True
pos += 1
elif parts[pos].startswith('fails-if'):
cond = parts[pos][len('fails-if('):-1]
if xul_tester.test(cond):
testcase.expect = False
pos += 1
elif parts[pos].startswith('asserts-if'):
# This directive means we may flunk some number of
# NS_ASSERTIONs in the browser. For the shell, ignore it.
pos += 1
elif parts[pos].startswith('skip-if'):
cond = parts[pos][len('skip-if('):-1]
if xul_tester.test(cond):
testcase.expect = testcase.enable = False
pos += 1
elif parts[pos].startswith('random-if'):
cond = parts[pos][len('random-if('):-1]
if xul_tester.test(cond):
testcase.random = True
pos += 1
elif parts[pos] == 'slow':
testcase.slow = True
pos += 1
elif parts[pos] == 'silentfail':
# silentfails use tons of memory, and Darwin doesn't support ulimit.
if xul_tester.test("xulRuntime.OS == 'Darwin'"):
testcase.expect = testcase.enable = False
pos += 1
else:
print('warning: invalid manifest line element "{}"'.format(
parts[pos]))
pos += 1
def _build_manifest_script_entry(script_name, test):
line = []
if test.terms:
line.append(test.terms)
line.append("script")
line.append(script_name)
if test.comment:
line.append("#")
line.append(test.comment)
return ' '.join(line)
def _map_prefixes_left(test_gen):
"""
Splits tests into a dictionary keyed on the first component of the test
path, aggregating tests with a common base path into a list.
"""
byprefix = {}
for t in test_gen:
left, sep, remainder = t.path.partition(os.sep)
if left not in byprefix:
byprefix[left] = []
if remainder:
t.path = remainder
byprefix[left].append(t)
return byprefix
def _emit_manifest_at(location, relative, test_gen, depth):
"""
location - str: absolute path where we want to write the manifest
relative - str: relative path from topmost manifest directory to current
test_gen - (str): generator of all test paths and directorys
depth - int: number of dirs we are below the topmost manifest dir
"""
manifests = _map_prefixes_left(test_gen)
filename = os.path.join(location, 'jstests.list')
manifest = []
numTestFiles = 0
for k, test_list in manifests.iteritems():
fullpath = os.path.join(location, k)
if os.path.isdir(fullpath):
manifest.append("include " + k + "/jstests.list")
relpath = os.path.join(relative, k)
_emit_manifest_at(fullpath, relpath, test_list, depth + 1)
else:
numTestFiles += 1
if len(test_list) != 1:
import pdb; pdb.set_trace()
assert len(test_list) == 1
line = _build_manifest_script_entry(k, test_list[0])
manifest.append(line)
# Always present our manifest in sorted order.
manifest.sort()
# If we have tests, we have to set the url-prefix so reftest can find them.
if numTestFiles > 0:
manifest = ["url-prefix {}jsreftest.html?test={}/".format(
'../' * depth, relative)] + manifest
fp = open(filename, 'w')
try:
fp.write('\n'.join(manifest) + '\n')
finally:
fp.close()
def make_manifests(location, test_gen):
_emit_manifest_at(location, '', test_gen, 0)
def _find_all_js_files(base, location):
for root, dirs, files in os.walk(location):
root = root[len(base) + 1:]
for fn in files:
if fn.endswith('.js'):
yield root, fn
TEST_HEADER_PATTERN_INLINE = re.compile(r'//\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?$')
TEST_HEADER_PATTERN_MULTI = re.compile(r'/\*\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?\*/')
def _parse_test_header(fullpath, testcase, xul_tester):
"""
This looks a bit weird. The reason is that it needs to be efficient, since
it has to be done on every test
"""
fp = open(fullpath, 'r')
try:
buf = fp.read(512)
finally:
fp.close()
# Bail early if we do not start with a single comment.
if not buf.startswith("//"):
return
# Extract the token.
buf, _, _ = buf.partition('\n')
matches = TEST_HEADER_PATTERN_INLINE.match(buf)
if not matches:
matches = TEST_HEADER_PATTERN_MULTI.match(buf)
if not matches:
return
testcase.tag = matches.group(1)
testcase.terms = matches.group(2)
testcase.comment = matches.group(4)
_parse_one(testcase, xul_tester)
def _parse_external_manifest(filename, relpath):
"""
Reads an external manifest file for test suites whose individual test cases
can't be decorated with reftest comments.
filename - str: name of the manifest file
relpath - str: relative path of the directory containing the manifest
within the test suite
"""
entries = []
with open(filename, 'r') as fp:
manifest_re = re.compile(r'^\s*(.*)\s+(include|script)\s+(\S+)$')
for line in fp:
line, _, comment = line.partition('#')
line = line.strip()
if not line:
continue
matches = manifest_re.match(line)
if not matches:
print('warning: unrecognized line in jstests.list:'
' {0}'.format(line))
continue
path = os.path.normpath(os.path.join(relpath, matches.group(3)))
if matches.group(2) == 'include':
# The manifest spec wants a reference to another manifest here,
# but we need just the directory. We do need the trailing
# separator so we don't accidentally match other paths of which
# this one is a prefix.
assert(path.endswith('jstests.list'))
path = path[:-len('jstests.list')]
entries.append({'path': path, 'terms': matches.group(1),
'comment': comment.strip()})
# if one directory name is a prefix of another, we want the shorter one
# first
entries.sort(key=lambda x: x["path"])
return entries
def _apply_external_manifests(filename, testcase, entries, xul_tester):
for entry in entries:
if filename.startswith(entry["path"]):
# The reftest spec would require combining the terms (failure types)
# that may already be defined in the test case with the terms
# specified in entry; for example, a skip overrides a random, which
# overrides a fails. Since we don't necessarily know yet in which
# environment the test cases will be run, we'd also have to
# consider skip-if, random-if, and fails-if with as-yet unresolved
# conditions.
# At this point, we use external manifests only for test cases
# that can't have their own failure type comments, so we simply
# use the terms for the most specific path.
testcase.terms = entry["terms"]
testcase.comment = entry["comment"]
_parse_one(testcase, xul_tester)
def _is_test_file(path_from_root, basename, filename, requested_paths,
excluded_paths):
# Any file whose basename matches something in this set is ignored.
EXCLUDED = set(('browser.js', 'shell.js', 'jsref.js', 'template.js',
'user.js', 'sta.js',
'test262-browser.js', 'test262-shell.js',
'test402-browser.js', 'test402-shell.js',
'testBuiltInObject.js', 'testIntl.js',
'js-test-driver-begin.js', 'js-test-driver-end.js'))
# Skip js files in the root test directory.
if not path_from_root:
return False
# Skip files that we know are not tests.
if basename in EXCLUDED:
return False
# If any tests are requested by name, skip tests that do not match.
if requested_paths \
and not any(req in filename for req in requested_paths):
return False
# Skip excluded tests.
if filename in excluded_paths:
return False
return True
def count_tests(location, requested_paths, excluded_paths):
count = 0
for root, basename in _find_all_js_files(location, location):
filename = os.path.join(root, basename)
if _is_test_file(root, basename, filename, requested_paths, excluded_paths):
count += 1
return count
def load_reftests(location, requested_paths, excluded_paths, xul_tester, reldir=''):
"""
Locates all tests by walking the filesystem starting at |location|.
Uses xul_tester to evaluate any test conditions in the test header.
Failure type and comment for a test case can come from
- an external manifest entry for the test case,
- an external manifest entry for a containing directory,
- most commonly: the header of the test case itself.
"""
manifestFile = os.path.join(location, 'jstests.list')
externalManifestEntries = _parse_external_manifest(manifestFile, '')
for root, basename in _find_all_js_files(location, location):
# Get the full path and relative location of the file.
filename = os.path.join(root, basename)
if not _is_test_file(root, basename, filename, requested_paths, excluded_paths):
continue
# Skip empty files.
fullpath = os.path.join(location, filename)
statbuf = os.stat(fullpath)
testcase = RefTestCase(os.path.join(reldir, filename))
_apply_external_manifests(filename, testcase, externalManifestEntries,
xul_tester)
_parse_test_header(fullpath, testcase, xul_tester)
yield testcase