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 324234 aa434447a11bd8b62b954b24005729a1bd757679
parent 324233 4c7373c6c29efffa4e7f1f87acbf755d9fae8f0d
child 324235 b641d01138ab54ec1f6b29232614b338b02d42f1
push id9671
push userraliiev@mozilla.com
push dateMon, 06 Jun 2016 20:27:52 +0000
treeherdermozilla-aurora@cea65ca3d0bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersterrence
bugs1259850
milestone49.0a1
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]