Bug 1259850 - Rewrite the test suite, add several tests, r=terrence

MozReview-Commit-ID: HtJ0uA3IfXZ

--HG--
rename : js/src/devtools/rootAnalysis/run-test.py => js/src/devtools/rootAnalysis/t/sixgill.py
extra : rebase_source : ae42485def39c26798bee72a2544034d1a557d3c
This commit is contained in:
Steve Fink 2016-03-25 14:40:23 -07:00
parent 44e27262d8
commit cfed81ff96
9 changed files with 589 additions and 131 deletions

View File

@ -3,150 +3,84 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import sys
import os
import re
import json
import site
import subprocess
import argparse
testdir = os.path.abspath(os.path.dirname(__file__))
testdir = os.path.abspath(os.path.join(os.path.dirname(__file__), 't'))
site.addsitedir(testdir)
from testlib import Test, equal
cfg = {}
cfg['SIXGILL_ROOT'] = os.environ.get('SIXGILL',
os.path.join(testdir, "sixgill"))
cfg['SIXGILL_BIN'] = os.environ.get('SIXGILL_BIN',
os.path.join(cfg['SIXGILL_ROOT'], "usr", "bin"))
cfg['SIXGILL_PLUGIN'] = os.environ.get('SIXGILL_PLUGIN',
os.path.join(cfg['SIXGILL_ROOT'], "usr", "libexec", "sixgill", "gcc", "xgill.so"))
cfg['CC'] = os.environ.get("CC",
"gcc")
cfg['CXX'] = os.environ.get("CXX",
cfg.get('CC', 'g++'))
cfg['JS_BIN'] = os.environ["JS"]
scriptdir = os.path.abspath(os.path.dirname(__file__))
parser = argparse.ArgumentParser(description='run hazard analysis tests')
parser.add_argument(
'--js', default=os.environ.get('JS'),
help='JS binary to run the tests with')
parser.add_argument(
'--sixgill', default=os.environ.get('SIXGILL', os.path.join(testdir, "sixgill")),
help='Path to root of sixgill installation')
parser.add_argument(
'--sixgill-bin', default=os.environ.get('SIXGILL_BIN'),
help='Path to sixgill binary dir')
parser.add_argument(
'--sixgill-plugin', default=os.environ.get('SIXGILL_PLUGIN'),
help='Full path to sixgill gcc plugin')
parser.add_argument(
'--gccdir', default=os.environ.get('GCCDIR'),
help='Path to GCC installation dir')
parser.add_argument(
'--cc', default=os.environ.get('CC'),
help='Path to gcc')
parser.add_argument(
'--cxx', default=os.environ.get('CXX'),
help='Path to g++')
parser.add_argument(
'--verbose', '-v', action='store_true',
help='Display verbose output, including commands executed')
cfg = parser.parse_args()
if not cfg.js:
exit('Must specify JS binary through environment variable or --js option')
if not cfg.cc:
if cfg.gccdir:
cfg.cc = os.path.join(cfg.gccdir, "bin", "gcc")
else:
cfg.cc = "gcc"
if not cfg.cxx:
if cfg.gccdir:
cfg.cxx = os.path.join(cfg.gccdir, "bin", "g++")
else:
cfg.cxx = "g++"
if not cfg.sixgill_bin:
cfg.sixgill_bin = os.path.join(cfg.sixgill, "usr", "bin")
if not cfg.sixgill_plugin:
cfg.sixgill_plugin = os.path.join(cfg.sixgill, "usr", "libexec", "sixgill", "gcc", "xgill.so")
subprocess.check_call([cfg.js, '-e', 'if (!getBuildConfiguration()["has-ctypes"]) quit(1)'])
def binpath(prog):
return os.path.join(cfg['SIXGILL_BIN'], prog)
return os.path.join(cfg.sixgill_bin, prog)
if not os.path.exists("test-output"):
os.mkdir("test-output")
try:
os.mkdir(os.path.join('t', 'out'))
except OSError:
pass
# Simplified version of the body info.
class Body(dict):
def __init__(self, body):
self['BlockIdKind'] = body['BlockId']['Kind']
if 'Variable' in body['BlockId']:
self['BlockName'] = body['BlockId']['Variable']['Name'][0]
self['LineRange'] = [ body['Location'][0]['Line'], body['Location'][1]['Line'] ]
self['Filename'] = body['Location'][0]['CacheString']
self['Edges'] = body.get('PEdge', [])
self['Points'] = { i+1: body['PPoint'][i]['Location']['Line'] for i in range(len(body['PPoint'])) }
self['Index'] = body['Index']
self['Variables'] = { x['Variable']['Name'][0]: x['Type'] for x in body['DefineVariable'] }
# Indexes
self['Line2Points'] = {}
for point, line in self['Points'].items():
self['Line2Points'].setdefault(line, []).append(point)
self['SrcPoint2Edges'] = {}
for edge in self['Edges']:
(src, dst) = edge['Index']
self['SrcPoint2Edges'].setdefault(src, []).append(edge)
self['Line2Edges'] = {}
for (src, edges) in self['SrcPoint2Edges'].items():
line = self['Points'][src]
self['Line2Edges'].setdefault(line, []).extend(edges)
def edges_from_line(self, line):
return self['Line2Edges'][line]
def edge_from_line(self, line):
edges = self.edges_from_line(line)
assert(len(edges) == 1)
return edges[0]
def edges_from_point(self, point):
return self['SrcPoint2Edges'][point]
def edge_from_point(self, point):
edges = self.edges_from_point(point)
assert(len(edges) == 1)
return edges[0]
def assignment_point(self, varname):
for edge in self['Edges']:
if edge['Kind'] != 'Assign':
continue
dst = edge['Exp'][0]
if dst['Kind'] != 'Var':
continue
if dst['Variable']['Name'][0] == varname:
return edge['Index'][0]
raise Exception("assignment to variable %s not found" % varname)
def assignment_line(self, varname):
return self['Points'][self.assignment_point(varname)]
tests = ['test']
tests = ['sixgill-tree', 'suppression', 'hazards']
for name in tests:
indir = os.path.join(testdir, name)
outdir = os.path.join(testdir, "test-output", name)
if not os.path.exists(outdir):
outdir = os.path.join(testdir, 'out', name)
try:
os.mkdir(outdir)
except OSError:
pass
def compile(source):
cmd = "{CXX} -c {source} -fplugin={sixgill}".format(source=os.path.join(indir, source),
CXX=cfg['CXX'], sixgill=cfg['SIXGILL_PLUGIN'])
print("Running %s" % cmd)
subprocess.check_call(["sh", "-c", cmd])
def load_db_entry(dbname, pattern):
if not isinstance(pattern, basestring):
output = subprocess.check_output([binpath("xdbkeys"), dbname + ".xdb"])
entries = output.splitlines()
matches = [f for f in entries if re.search(pattern, f)]
if len(matches) == 0:
raise Exception("entry not found")
if len(matches) > 1:
raise Exception("multiple entries found")
pattern = matches[0]
output = subprocess.check_output([binpath("xdbfind"), "-json", dbname + ".xdb", pattern])
return json.loads(output)
def computeGCTypes():
file("defaults.py", "w").write('''\
analysis_scriptdir = '{testdir}'
sixgill_bin = '{bindir}'
'''.format(testdir=testdir, bindir=cfg['SIXGILL_BIN']))
cmd = [
os.path.join(testdir, "analyze.py"),
"gcTypes", "--upto", "gcTypes",
"--source=%s" % indir,
"--objdir=%s" % outdir,
"--js=%s" % cfg['JS_BIN'],
]
print("Running " + " ".join(cmd))
output = subprocess.check_call(cmd)
def loadGCTypes():
gctypes = {'GCThings': [], 'GCPointers': []}
for line in file(os.path.join(outdir, "gcTypes.txt")):
m = re.match(r'^(GC\w+): (.*)', line)
if m:
gctypes[m.group(1) + 's'].append(m.group(2))
return gctypes
def process_body(body):
return Body(body)
def process_bodies(bodies):
return [ process_body(b) for b in bodies ]
def equal(got, expected):
if got != expected:
print("Got '%s', expected '%s'" % (got, expected))
test = Test(indir, outdir, cfg)
os.chdir(outdir)
subprocess.call(["sh", "-c", "rm *.xdb"])
execfile(os.path.join(indir, "test.py"))
execfile(os.path.join(indir, "test.py"), {'test': test, 'equal': equal})
print("TEST-PASSED: %s" % name)

View File

@ -0,0 +1,89 @@
#define ANNOTATE(property) __attribute__((tag(property)))
struct Cell { int f; } ANNOTATE("GC Thing");
class AutoSuppressGC_Base {
public:
AutoSuppressGC_Base() {}
~AutoSuppressGC_Base() {}
} ANNOTATE("Suppress GC");
class AutoSuppressGC_Child : public AutoSuppressGC_Base {
public:
AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
};
class AutoSuppressGC {
AutoSuppressGC_Child helpImBeingSuppressed;
public:
AutoSuppressGC() {}
};
extern void GC() ANNOTATE("GC Call");
extern void invisible();
void GC()
{
// If the implementation is too trivial, the function body won't be emitted at all.
asm("");
invisible();
}
extern void foo(Cell*);
void suppressedFunction() {
GC(); // Calls GC, but is always called within AutoSuppressGC
}
void halfSuppressedFunction() {
GC(); // Calls GC, but is sometimes called within AutoSuppressGC
}
void unsuppressedFunction() {
GC(); // Calls GC, never within AutoSuppressGC
}
volatile static int x = 3;
volatile static int* xp = &x;
struct GCInDestructor {
~GCInDestructor() {
invisible();
asm("");
*xp = 4;
GC();
}
};
Cell*
f()
{
GCInDestructor kaboom;
Cell cell;
Cell* cell1 = &cell;
Cell* cell2 = &cell;
Cell* cell3 = &cell;
Cell* cell4 = &cell;
{
AutoSuppressGC nogc;
suppressedFunction();
halfSuppressedFunction();
}
foo(cell1);
halfSuppressedFunction();
foo(cell2);
unsuppressedFunction();
{
// Old bug: it would look from the first AutoSuppressGC constructor it
// found to the last destructor. This statement *should* have no effect.
AutoSuppressGC nogc;
}
foo(cell3);
Cell* cell5 = &cell;
foo(cell5);
// Hazard in return value due to ~GCInDestructor
Cell* cell6 = &cell;
return cell6;
}

View File

@ -0,0 +1,36 @@
test.compile("source.cpp")
test.run_analysis_script('gcTypes')
# gcFunctions should be the inverse, but we get to rely on unmangled names here.
gcFunctions = test.load_gcFunctions()
print(gcFunctions)
assert('void GC()' in gcFunctions)
assert('void suppressedFunction()' not in gcFunctions)
assert('void halfSuppressedFunction()' in gcFunctions)
assert('void unsuppressedFunction()' in gcFunctions)
assert('Cell* f()' in gcFunctions)
hazards = test.load_hazards()
hazmap = {haz.variable: haz for haz in hazards}
assert('cell1' not in hazmap)
assert('cell2' in hazmap)
assert('cell3' in hazmap)
assert('cell4' not in hazmap)
assert('cell5' not in hazmap)
assert('cell6' not in hazmap)
assert('<returnvalue>' in hazmap)
# All hazards should be in f()
assert(hazmap['cell2'].function == 'Cell* f()')
assert(len(set(haz.function for haz in hazards)) == 1)
# Check that the correct GC call is reported for each hazard. (cell3 has a
# hazard from two different GC calls; it doesn't really matter which is
# reported.)
assert(hazmap['cell2'].GCFunction == 'void halfSuppressedFunction()')
assert(hazmap['cell3'].GCFunction in ('void halfSuppressedFunction()', 'void unsuppressedFunction()'))
assert(hazmap['<returnvalue>'].GCFunction == 'void GCInDestructor::~GCInDestructor()')
# Type names are handy to have in the report.
assert(hazmap['cell2'].type == 'Cell*')
assert(hazmap['<returnvalue>'].type == 'Cell*')

View File

@ -0,0 +1,70 @@
#define ANNOTATE(property) __attribute__((tag(property)))
namespace js {
namespace gc {
struct Cell { int f; } ANNOTATE("GC Thing");
}
}
struct Bogon {
};
struct JustACell : public js::gc::Cell {
bool iHaveNoDataMembers() { return true; }
};
struct JSObject : public js::gc::Cell, public Bogon {
int g;
};
struct SpecialObject : public JSObject {
int z;
};
struct ErrorResult {
bool hasObj;
JSObject *obj;
void trace() {}
} ANNOTATE("Suppressed GC Pointer");
struct OkContainer {
ErrorResult res;
bool happy;
};
struct UnrootedPointer {
JSObject *obj;
};
template <typename T>
class Rooted {
T data;
} ANNOTATE("Rooted Pointer");
extern void js_GC() ANNOTATE("GC Call") ANNOTATE("Slow");
void js_GC() {}
void root_arg(JSObject *obj, JSObject *random)
{
// Use all these types so they get included in the output.
SpecialObject so;
UnrootedPointer up;
Bogon b;
OkContainer okc;
Rooted<JSObject*> ro;
Rooted<SpecialObject*> rso;
obj = random;
JSObject *other1 = obj;
js_GC();
float MARKER1 = 0;
JSObject *other2 = obj;
other1->f = 1;
other2->f = -1;
unsigned int u1 = 1;
unsigned int u2 = -1;
}

View File

@ -0,0 +1,60 @@
import re
test.compile("source.cpp")
test.computeGCTypes()
body = test.process_body(test.load_db_entry("src_body", re.compile(r'root_arg'))[0])
# Rendering positive and negative integers
marker1 = body.assignment_line('MARKER1')
equal(body.edge_from_line(marker1 + 2)['Exp'][1]['String'], '1')
equal(body.edge_from_line(marker1 + 3)['Exp'][1]['String'], '-1')
equal(body.edge_from_point(body.assignment_point('u1'))['Exp'][1]['String'], '1')
equal(body.edge_from_point(body.assignment_point('u2'))['Exp'][1]['String'], '4294967295')
assert('obj' in body['Variables'])
assert('random' in body['Variables'])
assert('other1' in body['Variables'])
assert('other2' in body['Variables'])
# Test function annotations
js_GC = test.process_body(test.load_db_entry("src_body", re.compile(r'js_GC'))[0])
annotations = js_GC['Variables']['void js_GC()']['Annotation']
assert(annotations)
found_call_tag = False
for annotation in annotations:
(annType, value) = annotation['Name']
if annType == 'Tag' and value == 'GC Call':
found_call_tag = True
assert(found_call_tag)
# Test type annotations
# js::gc::Cell first
cell = test.load_db_entry("src_comp", 'js::gc::Cell')[0]
assert(cell['Kind'] == 'Struct')
annotations = cell['Annotation']
assert(len(annotations) == 1)
(tag, value) = annotations[0]['Name']
assert(tag == 'Tag')
assert(value == 'GC Thing')
# Check JSObject inheritance.
JSObject = test.load_db_entry("src_comp", 'JSObject')[0]
bases = [ b['Base'] for b in JSObject['CSUBaseClass'] ]
assert('js::gc::Cell' in bases)
assert('Bogon' in bases)
assert(len(bases) == 2)
# Check type analysis
gctypes = test.load_gcTypes()
assert('js::gc::Cell' in gctypes['GCThings'])
assert('JustACell' in gctypes['GCThings'])
assert('JSObject' in gctypes['GCThings'])
assert('SpecialObject' in gctypes['GCThings'])
assert('UnrootedPointer' in gctypes['GCPointers'])
assert('Bogon' not in gctypes['GCThings'])
assert('Bogon' not in gctypes['GCPointers'])
assert('ErrorResult' not in gctypes['GCPointers'])
assert('OkContainer' not in gctypes['GCPointers'])
assert('class Rooted<JSObject*>' not in gctypes['GCPointers'])

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from collections import defaultdict
# Simplified version of the body info.
class Body(dict):
def __init__(self, body):
self['BlockIdKind'] = body['BlockId']['Kind']
if 'Variable' in body['BlockId']:
self['BlockName'] = body['BlockId']['Variable']['Name'][0].split("$")[-1]
loc = body['Location']
self['LineRange'] = (loc[0]['Line'], loc[1]['Line'])
self['Filename'] = loc[0]['CacheString']
self['Edges'] = body.get('PEdge', [])
self['Points'] = { i: p['Location']['Line'] for i, p in enumerate(body['PPoint'], 1) }
self['Index'] = body['Index']
self['Variables'] = { x['Variable']['Name'][0].split("$")[-1]: x['Type'] for x in body['DefineVariable'] }
# Indexes
self['Line2Points'] = defaultdict(list)
for point, line in self['Points'].items():
self['Line2Points'][line].append(point)
self['SrcPoint2Edges'] = defaultdict(list)
for edge in self['Edges']:
src, dst = edge['Index']
self['SrcPoint2Edges'][src].append(edge)
self['Line2Edges'] = defaultdict(list)
for (src, edges) in self['SrcPoint2Edges'].items():
line = self['Points'][src]
self['Line2Edges'][line].extend(edges)
def edges_from_line(self, line):
return self['Line2Edges'][line]
def edge_from_line(self, line):
edges = self.edges_from_line(line)
assert(len(edges) == 1)
return edges[0]
def edges_from_point(self, point):
return self['SrcPoint2Edges'][point]
def edge_from_point(self, point):
edges = self.edges_from_point(point)
assert(len(edges) == 1)
return edges[0]
def assignment_point(self, varname):
for edge in self['Edges']:
if edge['Kind'] != 'Assign':
continue
dst = edge['Exp'][0]
if dst['Kind'] != 'Var':
continue
if dst['Variable']['Name'][0] == varname:
return edge['Index'][0]
raise Exception("assignment to variable %s not found" % varname)
def assignment_line(self, varname):
return self['Points'][self.assignment_point(varname)]

View File

@ -0,0 +1,64 @@
#define ANNOTATE(property) __attribute__((tag(property)))
struct Cell { int f; } ANNOTATE("GC Thing");
class AutoSuppressGC_Base {
public:
AutoSuppressGC_Base() {}
~AutoSuppressGC_Base() {}
} ANNOTATE("Suppress GC");
class AutoSuppressGC_Child : public AutoSuppressGC_Base {
public:
AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
};
class AutoSuppressGC {
AutoSuppressGC_Child helpImBeingSuppressed;
public:
AutoSuppressGC() {}
};
extern void GC() ANNOTATE("GC Call");
void GC()
{
// If the implementation is too trivial, the function body won't be emitted at all.
asm("");
}
extern void foo(Cell*);
void suppressedFunction() {
GC(); // Calls GC, but is always called within AutoSuppressGC
}
void halfSuppressedFunction() {
GC(); // Calls GC, but is sometimes called within AutoSuppressGC
}
void unsuppressedFunction() {
GC(); // Calls GC, never within AutoSuppressGC
}
void f() {
Cell* cell1 = nullptr;
Cell* cell2 = nullptr;
Cell* cell3 = nullptr;
{
AutoSuppressGC nogc;
suppressedFunction();
halfSuppressedFunction();
}
foo(cell1);
halfSuppressedFunction();
foo(cell2);
unsuppressedFunction();
{
// Old bug: it would look from the first AutoSuppressGC constructor it
// found to the last destructor. This statement *should* have no effect.
AutoSuppressGC nogc;
}
foo(cell3);
}

View File

@ -0,0 +1,23 @@
test.compile("source.cpp")
test.run_analysis_script('gcTypes', upto='gcFunctions')
# The suppressions file uses only mangled names since it's for internal use,
# though I may change that soon given (1) the unfortunate non-uniqueness of
# mangled constructor names, and (2) the usefulness of this file for
# mrgiggles's reporting.
suppressed = test.load_suppressed_functions()
# Only one of these is fully suppressed (ie, *always* called within the scope
# of an AutoSuppressGC).
assert(len(filter(lambda f: 'suppressedFunction' in f, suppressed)) == 1)
assert(len(filter(lambda f: 'halfSuppressedFunction' in f, suppressed)) == 0)
assert(len(filter(lambda f: 'unsuppressedFunction' in f, suppressed)) == 0)
# gcFunctions should be the inverse, but we get to rely on unmangled names here.
gcFunctions = test.load_gcFunctions()
print(gcFunctions)
assert('void GC()' in gcFunctions)
assert('void suppressedFunction()' not in gcFunctions)
assert('void halfSuppressedFunction()' in gcFunctions)
assert('void unsuppressedFunction()' in gcFunctions)
assert('void f()' in gcFunctions)

View File

@ -0,0 +1,119 @@
import json
import os
import re
import subprocess
from sixgill import Body
from collections import defaultdict, namedtuple
scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
HazardSummary = namedtuple('HazardSummary', ['function', 'variable', 'type', 'GCFunction', 'location'])
def equal(got, expected):
if got != expected:
print("Got '%s', expected '%s'" % (got, expected))
def extract_unmangled(func):
return func.split('$')[-1]
class Test(object):
def __init__(self, indir, outdir, cfg):
self.indir = indir
self.outdir = outdir
self.cfg = cfg
def infile(self, path):
return os.path.join(self.indir, path)
def binpath(self, prog):
return os.path.join(self.cfg.sixgill_bin, prog)
def compile(self, source):
cmd = "{CXX} -c {source} -O3 -std=c++11 -fplugin={sixgill} -fplugin-arg-xgill-mangle=1".format(
source=self.infile(source),
CXX=self.cfg.cxx, sixgill=self.cfg.sixgill_plugin)
if self.cfg.verbose:
print("Running %s" % cmd)
subprocess.check_call(["sh", "-c", cmd])
def load_db_entry(self, dbname, pattern):
'''Look up an entry from an XDB database file, 'pattern' may be an exact
matching string, or an re pattern object matching a single entry.'''
if not isinstance(pattern, basestring):
output = subprocess.check_output([self.binpath("xdbkeys"), dbname + ".xdb"])
matches = filter(lambda _: re.search(pattern, _), output.splitlines())
if len(matches) == 0:
raise Exception("entry not found")
if len(matches) > 1:
raise Exception("multiple entries found")
pattern = matches[0]
output = subprocess.check_output([self.binpath("xdbfind"), "-json", dbname + ".xdb", pattern])
return json.loads(output)
def run_analysis_script(self, phase, upto=None):
file("defaults.py", "w").write('''\
analysis_scriptdir = '{scriptdir}'
sixgill_bin = '{bindir}'
'''.format(scriptdir=scriptdir, bindir=self.cfg.sixgill_bin))
cmd = [os.path.join(scriptdir, "analyze.py"), phase]
if upto:
cmd += ["--upto", upto]
cmd.append("--source=%s" % self.indir)
cmd.append("--objdir=%s" % self.outdir)
cmd.append("--js=%s" % self.cfg.js)
if self.cfg.verbose:
cmd.append("--verbose")
print("Running " + " ".join(cmd))
subprocess.check_call(cmd)
def computeGCTypes(self):
self.run_analysis_script("gcTypes", upto="gcTypes")
def computeHazards(self):
self.run_analysis_script("callgraph")
def load_text_file(self, filename, extract=lambda l: l):
fullpath = os.path.join(self.outdir, filename)
values = (extract(line.strip()) for line in file(fullpath))
return filter(lambda _: _ is not None, values)
def load_suppressed_functions(self):
return set(self.load_text_file("suppressedFunctions.lst"))
def load_gcTypes(self):
def grab_type(line):
m = re.match(r'^(GC\w+): (.*)', line)
if m:
return (m.group(1) + 's', m.group(2))
return None
gctypes = defaultdict(list)
for collection, typename in self.load_text_file('gcTypes.txt', extract=grab_type):
gctypes[collection].append(typename)
return gctypes
def load_gcFunctions(self):
return self.load_text_file('gcFunctions.lst', extract=extract_unmangled)
def load_hazards(self):
def grab_hazard(line):
m = re.match(r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", line)
if m:
info = list(m.groups())
info[0] = info[0].split("$")[-1]
info[3] = info[3].split("$")[-1]
return HazardSummary(*info)
return None
return self.load_text_file('rootingHazards.txt', extract=grab_hazard)
def process_body(self, body):
return Body(body)
def process_bodies(self, bodies):
return [self.process_body(b) for b in bodies]