Bug 1259850 - Rewrite the test suite, add several tests, r=terrence
☠☠ backed out by 69518db96a4d ☠ ☠
authorSteve Fink <sfink@mozilla.com>
Fri, 25 Mar 2016 14:40:23 -0700
changeset 340771 aa434447a11bd8b62b954b24005729a1bd757679
parent 340770 4c7373c6c29efffa4e7f1f87acbf755d9fae8f0d
child 340772 b641d01138ab54ec1f6b29232614b338b02d42f1
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [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]