Bug 1259850 - Rewrite the test suite, add several tests, r=terrence
authorSteve Fink <sfink@mozilla.com>
Fri, 25 Mar 2016 14:40:23 -0700
changeset 338799 e51f08275a6694986d037c2547018563b7e947bd
parent 338798 8d85ed16c20e633fb14c17d37ac739750424ec2e
child 338800 810a121696ab26247926c95b7f34070b811e45aa
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersterrence
bugs1259850
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1259850 - Rewrite the test suite, add several tests, r=terrence MozReview-Commit-ID: HtJ0uA3IfXZ
js/src/devtools/rootAnalysis/run-test.py
js/src/devtools/rootAnalysis/t/hazards/source.cpp
js/src/devtools/rootAnalysis/t/hazards/test.py
js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
js/src/devtools/rootAnalysis/t/sixgill.py
js/src/devtools/rootAnalysis/t/suppression/source.cpp
js/src/devtools/rootAnalysis/t/suppression/test.py
js/src/devtools/rootAnalysis/t/testlib.py
--- a/js/src/devtools/rootAnalysis/run-test.py
+++ b/js/src/devtools/rootAnalysis/run-test.py
@@ -1,152 +1,86 @@
 #!/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/.
 
-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
+
+scriptdir = os.path.abspath(os.path.dirname(__file__))
 
-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"]
+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)
-
-if not os.path.exists("test-output"):
-    os.mkdir("test-output")
-
-# 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)
+    return os.path.join(cfg.sixgill_bin, prog)
 
-    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]
+try:
+    os.mkdir(os.path.join('t', 'out'))
+except OSError:
+    pass
 
-    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)
-
-    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)
+    except OSError:
+        pass
 
-    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)
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
@@ -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;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/test.py
@@ -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*')
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
@@ -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;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
@@ -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'])
copy from js/src/devtools/rootAnalysis/run-test.py
copy to js/src/devtools/rootAnalysis/t/sixgill.py
--- a/js/src/devtools/rootAnalysis/run-test.py
+++ b/js/src/devtools/rootAnalysis/t/sixgill.py
@@ -1,65 +1,41 @@
 #!/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/.
 
-import sys
-import os
-import re
-import json
-import subprocess
-
-testdir = os.path.abspath(os.path.dirname(__file__))
-
-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"]
-
-def binpath(prog):
-    return os.path.join(cfg['SIXGILL_BIN'], prog)
-
-if not os.path.exists("test-output"):
-    os.mkdir("test-output")
+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]
-        self['LineRange'] = [ body['Location'][0]['Line'], body['Location'][1]['Line'] ]
-        self['Filename'] = body['Location'][0]['CacheString']
+            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+1: body['PPoint'][i]['Location']['Line'] for i in range(len(body['PPoint'])) }
+        self['Points'] = { i: p['Location']['Line'] for i, p in enumerate(body['PPoint'], 1) }
         self['Index'] = body['Index']
-        self['Variables'] = { x['Variable']['Name'][0]: x['Type'] for x in body['DefineVariable'] }
+        self['Variables'] = { x['Variable']['Name'][0].split("$")[-1]: x['Type'] for x in body['DefineVariable'] }
 
         # Indexes
-        self['Line2Points'] = {}
+        self['Line2Points'] = defaultdict(list)
         for point, line in self['Points'].items():
-            self['Line2Points'].setdefault(line, []).append(point)
-        self['SrcPoint2Edges'] = {}
+            self['Line2Points'][line].append(point)
+        self['SrcPoint2Edges'] = defaultdict(list)
         for edge in self['Edges']:
-            (src, dst) = edge['Index']
-            self['SrcPoint2Edges'].setdefault(src, []).append(edge)
-        self['Line2Edges'] = {}
+            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'].setdefault(line, []).extend(edges)
+            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]
@@ -80,73 +56,8 @@ class Body(dict):
             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']
-for name in tests:
-    indir = os.path.join(testdir, name)
-    outdir = os.path.join(testdir, "test-output", name)
-    if not os.path.exists(outdir):
-        os.mkdir(outdir)
-
-    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))
-
-    os.chdir(outdir)
-    subprocess.call(["sh", "-c", "rm *.xdb"])
-    execfile(os.path.join(indir, "test.py"))
-    print("TEST-PASSED: %s" % name)
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
@@ -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);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/test.py
@@ -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)
new file mode 100644
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -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]