Bug 1224691 - Parse lcov files and rewrite them based on preprocessor info. draft
authorChris Manchester <cmanchester@mozilla.com>
Wed, 27 Jan 2016 13:55:42 -0800
changeset 326411 15934fda942704ab918b9f43aea27f2f3f61f335
parent 326410 13f2a149a0229d74df08e0224b8c3227f05aa7c4
child 513607 26cabfff2b2537c50a5c8ef4d4aab6b84c3f7f7a
push id10145
push usercmanchester@mozilla.com
push dateWed, 27 Jan 2016 21:56:10 +0000
bugs1224691
milestone47.0a1
Bug 1224691 - Parse lcov files and rewrite them based on preprocessor info.
python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
python/mozbuild/mozbuild/codecoverage/test/sample_lcov.info
python/mozbuild/mozbuild/codecoverage/test/test_lcov_rewrite.py
--- a/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
+++ b/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
@@ -3,26 +3,422 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from argparse import ArgumentParser
 import json
 import os
 import re
 import sys
 import urlparse
+from collections import defaultdict
 
 from mozpack.copier import FileRegistry
 from mozpack.files import PreprocessedFile
 from mozpack.manifests import InstallManifest
 from mozpack.chrome.manifest import parse_manifest
 from chrome_map import ChromeManifestHandler
 
 import buildconfig
 import mozpack.path as mozpath
 
+class LcovRecord(object):
+    __slots__ = ("test_name",
+                 "source_file",
+                 "functions",
+                 "function_exec_counts",
+                 "function_count",
+                 "covered_function_count",
+                 "branches",
+                 "branch_count",
+                 "covered_branch_count",
+                 "lines",
+                 "line_count",
+                 "covered_line_count")
+    def __init__(self):
+        self.functions = {}
+        self.function_exec_counts = {}
+        self.branches = {}
+        self.lines = {}
+
+    def __iadd__(self, other):
+
+        # These shouldn't differ.
+        self.source_file = other.source_file
+        if hasattr(other, 'test_name'):
+            self.test_name = other.test_name
+        self.functions.update(other.functions)
+
+        for name, count in other.function_exec_counts.iteritems():
+            new_count = count
+            if name in self.function_exec_counts:
+                new_count = int(count) + int(self.function_exec_counts[name])
+            self.function_exec_counts[name] = new_count
+
+        for key, taken in other.branches.iteritems():
+            new_taken = 0 if taken == '-' else int(taken)
+            if key in self.branches:
+                old_taken = (0 if self.branches[key] == '-'
+                             else int(self.branches[key]))
+                new_taken += old_taken
+            self.branches[key] = new_taken
+
+        for line, (exec_count, checksum) in other.lines.iteritems():
+            new_exec_count = int(exec_count)
+            if line in self.lines:
+                old_exec_count, _ = self.lines[line]
+                new_exec_count += int(old_exec_count)
+            self.lines[line] = new_exec_count, checksum
+
+        self.resummarize()
+        return self
+
+    def resummarize(self):
+        # Re-calculate summaries after generating or splitting a record.
+        self.function_count = len(self.functions.keys())
+        # Function records may have moved between files, so filter here.
+        self.function_exec_counts = {fn_name: count for fn_name, count in self.function_exec_counts.iteritems()
+                                     if fn_name in self.functions.values()}
+        self.covered_function_count = len([c for c in self.function_exec_counts.values()
+                                           if c not in ('0', 0)])
+        self.line_count = len(self.lines)
+        self.covered_line_count = len([c for c, _ in self.lines.values()
+                                       if c not in (0, '0')])
+        self.branch_count = len(self.branches)
+        self.covered_branch_count = len([c for c in self.branches.values()
+                                         if c not in ('-', '0', 0)])
+
+class RecordRewriter(object):
+    # Helper class for rewriting/spliting individual lcov records according
+    # to what the preprocessor did.
+    def __init__(self):
+        self.pp_info = {}
+        self._ranges = None
+        self._line_comment_re = re.compile('^//@line (\d+) "(.+)"$')
+
+    @property
+    def additions(self):
+        added_records = self._additions.values()
+        for r in added_records:
+            r.resummarize()
+        return added_records
+
+    def populate_pp_info(self, fh, src_path):
+        if src_path in self.pp_info:
+            return
+
+        # (start, end) -> (included_source, start)
+        section_info = dict()
+
+        this_section = None
+
+        def finish_section(pp_end):
+            if this_section:
+                pp_start, inc_source, inc_start = this_section
+                key = pp_start, pp_end
+                section_info[key] = inc_source, inc_start
+
+        for count, line in enumerate(fh):
+            # Regex are quite slow, so bail out early.
+            if not line.startswith('//@line'):
+                continue
+            m = re.match(self._line_comment_re, line)
+            if m:
+                if this_section:
+                    finish_section(count + 1)
+                inc_start, inc_source = m.groups()
+                pp_start = count + 2
+                this_section = pp_start, inc_source, int(inc_start)
+
+        finish_section(count + 2)
+        self.pp_info[src_path] = section_info
+
+    def _get_range(self, line):
+        for start, end in self._ranges:
+            if line < start:
+                return None
+            if line < end:
+                return start, end
+        return None
+
+    def _get_mapped_line(self, line, r):
+        inc_source, inc_start = self._current_pp_info[r]
+        start, end = r
+        offs = line - start
+        return inc_start + offs
+
+    def _get_record(self, inc_source, additions):
+        if inc_source in additions:
+            gen_rec = additions[inc_source]
+        else:
+            gen_rec = LcovRecord()
+            gen_rec.source_file = inc_source
+            additions[inc_source] = gen_rec
+        return gen_rec
+
+    def _rewrite_lines(self, record, additions):
+        rewritten_lines = {}
+        for ln, line_info in record.lines.iteritems():
+            r = self._get_range(ln)
+            if r is None:
+                rewritten_lines[ln] = line_info
+                continue
+            # Get the offset in the mapped range.
+            new_ln = self._get_mapped_line(ln, r)
+            inc_source, _ = self._current_pp_info[r]
+
+            if inc_source != record.source_file:
+                gen_rec = self._get_record(inc_source, additions)
+                gen_rec.lines[new_ln] = line_info
+                continue
+
+            # Move exec_count to the new lineno.
+            rewritten_lines[new_ln] = line_info
+
+        record.lines = rewritten_lines
+
+    def _rewrite_functions(self, record, additions):
+        rewritten_fns = {}
+
+        # Sometimes we get multiple entries for a named function (top-level, for
+        # instance). It's not clear the records that result are well-formed, but
+        # we act as though if a function has multiple FN's, the corresponding
+        # FNDA's are all the same.
+        for ln, fn_name in record.functions.iteritems():
+            r = self._get_range(ln)
+            if r is None:
+                rewritten_fns[ln] = fn_name
+                continue
+            # Get the offset in the mapped range.
+            new_ln = self._get_mapped_line(ln, r)
+            inc_source, _ = self._current_pp_info[r]
+            if inc_source != record.source_file:
+                gen_rec = self._get_record(inc_source, additions)
+                gen_rec.functions[new_ln] = fn_name
+                if fn_name in record.function_exec_counts:
+                    gen_rec.function_exec_counts[fn_name] = record.function_exec_counts[fn_name]
+                continue
+            rewritten_fns[new_ln] = fn_name
+        record.functions = rewritten_fns
+
+    def _rewrite_branches(self, record, additions):
+        rewritten_branches = {}
+        for (ln, block_number, branch_number), taken in record.branches.iteritems():
+            r = self._get_range(ln)
+            if r is None:
+                rewritten_branches[ln, block_number, branch_number] = taken
+                continue
+            new_ln = self._get_mapped_line(ln, r)
+            inc_source, _ = self._current_pp_info[r]
+            if inc_source != record.source_file:
+                gen_rec = self._get_record(inc_source, additions)
+                gen_rec.branches[(new_ln, block_number, branch_number)] = taken
+                continue
+            rewritten_branches[(new_ln, block_number, branch_number)] = taken
+
+        record.branches = rewritten_branches
+
+    def rewrite_record(self, record):
+        # Rewrite the lines in the given record according to preprocessor info
+        # and split to additional records when pp_info has included file info.
+        self._current_pp_info = self.pp_info[record.source_file]
+        self._ranges = sorted(self._current_pp_info.keys())
+
+        additions = {}
+
+        self._rewrite_lines(record, additions)
+        self._rewrite_functions(record, additions)
+        self._rewrite_branches(record, additions)
+
+        record.resummarize()
+
+        self._ranges = None
+        self._current_pp_info = None
+        generated_records = additions.values()
+        for r in generated_records:
+            r.resummarize()
+        return generated_records
+
+class LcovFile(object):
+    # Simple parser/pretty-printer for lcov format.
+    # lcov parsing based on http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
+
+    # TN:<test name>
+    # SF:<absolute path to the source file>
+    # FN:<line number of function start>,<function name>
+    # FNDA:<execution count>,<function name>
+    # FNF:<number of functions found>
+    # FNH:<number of function hit>
+    # BRDA:<line number>,<block number>,<branch number>,<taken>
+    # BRF:<number of branches found>
+    # BRH:<number of branches hit>
+    # DA:<line number>,<execution count>[,<checksum>]
+    # LF:<number of instrumented lines>
+    # LH:<number of lines with a non-zero execution count>
+    # end_of_record
+
+    def __init__(self, lcov_fh):
+        # These are keyed by source file because output will split sources (at
+        # least for xbl).
+        self._records = defaultdict(lambda: LcovRecord())
+        self.new_record()
+        self.parse_file(lcov_fh)
+
+    @property
+    def records(self):
+        return self._records.values()
+
+    @records.setter
+    def records(self, value):
+        self._records = {r.source_file: r for r in value}
+
+    def new_record(self):
+        rec = LcovRecord()
+        self.current_record = rec
+
+    def finish_record(self):
+        self._records[self.current_record.source_file] += self.current_record
+        self.new_record()
+
+    def parse_file(self, lcov_fh):
+        for count, line in enumerate(lcov_fh):
+            line = line.rstrip()
+            if not line:
+                continue
+            if line == 'end_of_record':
+                self.finish_record()
+                continue
+            colon = line.find(':')
+
+            prefix = line[:colon]
+
+            # We occasionally end up with multi-line scripts in data:
+            # uris that will trip up the parser, just skip them for now.
+            if colon < 0 or prefix not in ('TN', 'SF', 'FN', 'FNDA', 'FNF',
+                                           'FNH', 'BRDA', 'BRF', 'BRH', 'DA',
+                                           'LF', 'LH'):
+                continue
+            if prefix not in ('SF', 'TN'):
+                args = line[(colon + 1):].split(',')
+            else:
+                args = line[(colon + 1):],
+            try:
+                LcovFile.__dict__['parse_' + prefix](self, *args)
+            except ValueError:
+                print("Encountered an error at line %d:\n%s" %
+                      (count + 1, line))
+                raise
+            except KeyError:
+                print("Invalid lcov line start at %s:%d:\n%s" %
+                      (lcov_fh.name, count + 1, line))
+                raise
+
+    def print_file(self, fh):
+        for record in self.records:
+            fh.write(self.format_record(record))
+            fh.write('\n')
+
+    def format_record(self, record):
+        out_lines = []
+        for name in LcovRecord.__slots__:
+            if hasattr(record, name):
+                out_lines.append(LcovFile.__dict__['format_' + name](self, record))
+        return '\n'.join(out_lines) + '\nend_of_record'
+
+    def format_test_name(self, record):
+        return "TN:%s" % record.test_name
+
+    def format_source_file(self, record):
+        return "SF:%s" % record.source_file
+
+    def format_functions(self, record):
+        # Sorting results gives deterministic output (and is a lot faster than
+        # using OrderedDict).
+        fns = []
+        for start_lineno, fn_name in sorted(record.functions.iteritems()):
+            fns.append('FN:%s,%s' % (start_lineno, fn_name))
+        return '\n'.join(fns)
+
+    def format_function_exec_counts(self, record):
+        fndas = []
+        for name, exec_count in sorted(record.function_exec_counts.iteritems()):
+            fndas.append('FNDA:%s,%s' % (exec_count, name))
+        return '\n'.join(fndas)
+
+    def format_function_count(self, record):
+        return 'FNF:%s' % record.function_count
+
+    def format_covered_function_count(self, record):
+        return 'FNH:%s' % record.covered_function_count
+
+    def format_branches(self, record):
+        brdas = []
+        for key in sorted(record.branches):
+            brdas.append('BRDA:%s' %
+                         ','.join(map(str, list(key) + [record.branches[key]])))
+        return '\n'.join(brdas)
+
+    def format_branch_count(self, record):
+        return 'BRF:%s' % record.branch_count
+
+    def format_covered_branch_count(self, record):
+        return 'BRH:%s' % record.covered_branch_count
+
+    def format_lines(self, record):
+        das = []
+        for line_no, (exec_count, checksum) in sorted(record.lines.iteritems()):
+            s = 'DA:%s,%s' % (line_no, exec_count)
+            if checksum:
+                s += ',%s' % checksum
+            das.append(s)
+        return '\n'.join(das)
+
+    def format_line_count(self, record):
+        return 'LF:%s' % record.line_count
+
+    def format_covered_line_count(self, record):
+        return 'LH:%s' % record.covered_line_count
+
+    def parse_TN(self, test_name):
+        self.current_record.test_name = test_name
+
+    def parse_SF(self, source_file):
+        self.current_record.source_file = source_file
+
+    def parse_FN(self, start_lineno, fn_name):
+        self.current_record.functions[int(start_lineno)] = fn_name
+
+    def parse_FNDA(self, exec_count, fn_name):
+        self.current_record.function_exec_counts[fn_name] = exec_count
+
+    def parse_FNF(self, function_count):
+        self.current_record.function_count = function_count
+
+    def parse_FNH(self, covered_function_count):
+        self.current_record.covered_function_count = covered_function_count
+
+    def parse_BRDA(self, line_number, block_number, branch_number, taken):
+        self.current_record.branches[(int(line_number), block_number,
+                                      branch_number)] = taken
+
+    def parse_BRF(self, branch_count):
+        self.current_record.branch_count = branch_count
+
+    def parse_BRH(self, covered_branch_count):
+        self.current_record.covered_branch_count = covered_branch_count
+
+    def parse_DA(self, line_number, execution_count, checksum=None):
+        self.current_record.lines[int(line_number)] = (execution_count, checksum)
+
+    def parse_LH(self, covered_line_count):
+        self.current_record.covered_line_count = covered_line_count
+
+    def parse_LF(self, line_count):
+       self.current_record.line_count = line_count
+
+
 class UrlFinderError(Exception):
     pass
 
 class UrlFinder(object):
     # Given a "chrome://" or "resource://" url, uses data from the UrlMapBackend
     # and install manifests to find a path to the source file and the corresponding
     # (potentially pre-processed) file in the objdir.
     def __init__(self, appdir, gredir, extra_chrome_manifests):
@@ -219,44 +615,66 @@ class UrlFinder(object):
         return result
 
 class LcovFileRewriter(object):
     # Class for partial parses of LCOV format and rewriting to resolve urls
     # and preprocessed file lines.
     def __init__(self, appdir, gredir, extra_chrome_manifests):
         self.topobjdir = buildconfig.topobjdir
         self.url_finder = UrlFinder(appdir, gredir, extra_chrome_manifests)
-        self._line_comment_re = re.compile('//@line \d+ "(.+)"$')
+        self.pp_rewriter = RecordRewriter()
 
     def rewrite_file(self, in_path, output_suffix):
         in_path = os.path.abspath(in_path)
         out_path = in_path + output_suffix
-        skip_section = False
-        with open(in_path) as fh, open(out_path, 'w+') as out_fh:
-            for line in fh:
-                if skip_section:
-                    if line.rstrip() == 'end_of_record':
-                        skip_section = False
+
+        with open(in_path) as fh:
+            lcov_file = LcovFile(fh)
+
+        removals = set()
+        additions = []
+        unknowns = set()
+        pp = set()
+        for record in lcov_file.records:
+            url = record.source_file
+            try:
+                res = self.url_finder.rewrite_url(url)
+                if res is None:
+                    removals.add(record)
                     continue
-                if line.startswith('SF:'):
-                    url = line[3:].rstrip()
-                    try:
-                        res = self.url_finder.rewrite_url(url)
-                        if res is None:
-                            skip_section = True
-                            continue
-                    except UrlFinderError as e:
-                        print("Error: %s.\nCouldn't find source info for %s" %
-                              (e, url))
-                        skip_section = True
-                        continue
-                    src_file, objdir_file, _ = res
-                    assert os.path.isfile(src_file), "Couldn't find mapped source file at %s!" % src_file
-                    line = 'SF:%s\n' % src_file
-                out_fh.write(line)
+
+            except Exception as e:
+                if url not in unknowns:
+                    print("Error: %s.\nCouldn't find source info for %s, removing record" %
+                          (e, url))
+                removals.add(record)
+                unknowns.add(url)
+                continue
+            source_file, objdir_file, preprocessed = res
+            assert os.path.isfile(source_file), "Couldn't find mapped source file at %s!" % source_file
+            record.source_file = source_file
+            if preprocessed:
+                pp.add(record)
+                obj_path = os.path.join(self.topobjdir, objdir_file)
+                with open(obj_path) as fh:
+                    self.pp_rewriter.populate_pp_info(fh, source_file)
+
+        additions = []
+        for r in pp:
+            additions += self.pp_rewriter.rewrite_record(r)
+
+        lcov_file.records = [r for r in lcov_file.records + additions
+                             if r not in removals]
+        if not lcov_file.records:
+            print("WARNING: No valid records found in %s" % in_path)
+            return
+
+        with open(out_path, 'w+') as out_fh:
+            lcov_file.print_file(out_fh)
+
 
 def main():
     parser = ArgumentParser(description="Given a set of gcov .info files produced "
                             "by spidermonkey's code coverage, re-maps file urls "
                             "back to source files and lines in preprocessed files "
                             "back to their original locations.")
     parser.add_argument("--app-dir", default="dist/bin/browser/",
                         help="Prefix of the appdir in use. This is used to map "
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/codecoverage/test/sample_lcov.info
@@ -0,0 +1,1895 @@
+SF:/home/chris/m-c/browser/base/content/newtab/newTab.js
+FN:1,top-level
+FN:31,top-level
+FN:232,Transformation_rearrangeSites/</<
+FN:259,Transformation_whenTransitionEnded
+FN:262,onEnd
+FN:275,Transformation_getNodeOpacity
+FN:287,Transformation_setNodeOpacity
+FN:307,Transformation_moveSite
+FN:317,Transformation_isFrozen
+FN:334,Page_init
+FN:363,Page_observe
+FN:399,update
+FN:417,update/this._scheduleUpdateTimeout<
+FN:431,Page_init
+FN:443,Page_init/<
+FN:459,Page_updateAttributes
+FN:482,Page_handleUnloadEvent
+FN:499,Page_handleEvent
+FN:538,Page_handleEvent/<
+FN:544,gPage.onPageFirstVisible
+FN:567,onPageVisibleAndLoaded
+FN:575,reportLastVisibleTileIndex
+FN:619,gGrid.node
+FN:630,gGrid.cells
+FN:635,gGrid.sites
+FN:638,gGrid.ready
+FN:641,gGrid.isDocumentLoaded
+FN:647,Grid_init
+FN:651,Grid_init/<
+FN:676,Grid_createSite
+FN:685,Grid_handleEvent
+FN:697,Grid_lock
+FN:704,Grid_unlock
+FN:714,refresh
+FN:722,_refreshGrid
+FN:755,Grid_computeHeight
+FN:764,Grid_createSiteFragment
+FN:791,Grid_isHistoricalTile
+FN:799,Grid_resizeGrid
+FN:870,Cell
+FN:876,Cell/<
+FN:890,Cell.prototype.node
+FN:895,Cell.prototype.index
+FN:907,Cell.prototype.previousSibling
+FN:920,Cell.prototype.nextSibling
+FN:933,Cell.prototype.site
+FN:942,Cell_containsPinnedSite
+FN:951,Cell_isEmpty
+FN:958,Cell_handleEvent
+FN:991,Site
+FN:1005,Site.prototype.node
+FN:1010,Site.prototype.link
+FN:1015,Site.prototype.url
+FN:1020,Site.prototype.title
+FN:1025,Site.prototype.cell
+FN:1035,Site_pin
+FN:1051,Site_unpin
+FN:1063,Site_isPinned
+FN:1071,Site_block
+FN:1084,Site_querySelector
+FN:1093,Site.prototype._updateAttributes
+FN:1105,Site.prototype._newTabString
+FN:1116,Site.prototype._getSuggestedTileExplanation
+FN:1128,Site_checkLinkEndTime
+FN:1147,Site_render
+FN:1199,Site_onFirstVisible
+FN:1213,Site_captureIfMissing
+FN:1222,Site_refreshThumbnail
+FN:1246,Site.prototype._ignoreHoverEvents
+FN:1247,Site.prototype._ignoreHoverEvents/<
+FN:1250,Site.prototype._ignoreHoverEvents/<
+FN:1258,Site_addEventHandlers
+FN:1275,Site_speculativeConnect
+FN:1284,Site_recordSiteClicked
+FN:1296,Site.prototype._toggleLegalText
+FN:1324,Site_onClick
+FN:1386,Site_handleEvent
+FN:1417,gDrag.draggedSite
+FN:1424,gDrag.cellWidth
+FN:1425,gDrag.cellHeight
+FN:1432,Drag_start
+FN:1465,Drag_drag
+FN:1489,Drag_end
+FN:1505,Drag_isValid
+FN:1524,Drag_setDragData
+FN:1545,Drag_setDragData/<
+FN:1551,gDragDataHelper.mimeType
+FN:1555,DragDataHelper_getLinkFromDragEvent
+FN:1585,Drop_enter
+FN:1594,Drop_exit
+FN:1609,Drop_drop
+FN:1628,Drop_repinSitesAfterDrop
+FN:1632,Drop_repinSitesAfterDrop/pinnedSites<
+FN:1637,Drop_repinSitesAfterDrop/<
+FN:1645,Drop_pinDraggedSite
+FN:1669,Drop_delayedRearrange
+FN:1676,callback
+FN:1691,Drop_cancelDelayedArrange
+FN:1702,Drop_rearrange
+FN:1733,gDropTargetShim.init
+FN:1740,gDropTargetShim._addEventListeners
+FN:1752,gDropTargetShim._removeEventListeners
+FN:1764,gDropTargetShim.handleEvent
+FN:1788,gDropTargetShim._dragstart
+FN:1799,gDropTargetShim._dragover
+FN:1819,gDropTargetShim._drop
+FN:1839,gDropTargetShim._dragend
+FN:1862,gDropTargetShim._updateDropTarget
+FN:1888,gDropTargetShim._findDropTarget
+FN:1914,DropTargetShim_getCellPositions
+FN:1918,DropTargetShim_getCellPositions/this._cellPositions<
+FN:1929,gDropTargetShim._dispatchEvent
+FN:1954,DropPreview_rearrange
+FN:1972,DropPreview_insertDraggedSite
+FN:1999,DropPreview_repositionPinnedSites
+FN:2005,DropPreview_repositionPinnedSites/<
+FN:2023,DropPreview_filterPinnedSites
+FN:2030,DropPreview_filterPinnedSites/<
+FN:2047,DropPreview_getPinnedRange
+FN:2077,DropPreview_hasOverflowedPinnedSite
+FN:2104,DropPreview_repositionOverflowedPinnedSite
+FN:2135,DropPreview_indexOfLowerPrioritySite
+FN:2170,Updater_updateGrid
+FN:2177,Updater_updateGrid/<
+FN:2189,Updater_updateGrid/</<
+FN:2206,Updater_findRemainingSites
+FN:2210,Updater_findRemainingSites/<
+FN:2216,Updater_findRemainingSites/<
+FN:2225,Updater_freezeSitePositions
+FN:2226,Updater_freezeSitePositions/<
+FN:2236,Updater_moveSiteNodes
+FN:2244,Updater_moveSiteNodes/<
+FN:2268,Updater_rearrangeSites
+FN:2279,Updater_removeLegacySites
+FN:2283,Updater_removeLegacySites/<
+FN:2288,Updater_removeLegacySites/</<
+FN:2290,Updater_removeLegacySites/</</<
+FN:2308,Updater_fillEmptyCells
+FN:2312,Updater_fillEmptyCells/<
+FN:2316,Updater_fillEmptyCells/</<
+FN:2351,UndoDialog_init
+FN:2363,UndoDialog_show
+FN:2383,UndoDialog_hide
+FN:2399,UndoDialog_handleEvent
+FN:2416,UndoDialog_undo
+FN:2434,UndoDialog_undoAll
+FN:2435,UndoDialog_undoAll/<
+FN:2446,gSearch.init
+FN:2448,gSearch.init/<
+FN:2469,gCustomize.init
+FN:2474,gCustomize.init/<
+FN:2483,gCustomize.hidePanel
+FN:2484,onTransitionEnd
+FN:2495,gCustomize.showPanel
+FN:2504,gCustomize.showPanel/<
+FN:2517,gCustomize.handleEvent
+FN:2528,gCustomize.onClick
+FN:2554,gCustomize.onKeyDown
+FN:2560,gCustomize.showLearn
+FN:2565,gCustomize.updateSelected
+FN:2568,gCustomize.updateSelected/<
+FN:2602,gIntro.init
+FN:2608,gIntro._showMessage
+FN:2612,gIntro._showMessage/<
+FN:2621,gIntro._bold
+FN:2625,gIntro._link
+FN:2629,gIntro._exitIntro
+FN:2631,gIntro._exitIntro/<
+FN:2636,gIntro._generateParagraphs
+FN:2646,gIntro.showIfNecessary
+FN:2656,gIntro.showPanel
+FN:36,newTabString
+FN:44,inPrivateBrowsingMode
+FN:67,gTransformation._cellBorderWidths
+FN:86,Transformation_getNodePosition
+FN:96,Transformation_fadeNodeIn
+FN:97,Transformation_fadeNodeIn/<
+FN:111,Transformation_fadeNodeOut
+FN:120,Transformation_showSite
+FN:129,Transformation_hideSite
+FN:138,Transformation_setSitePosition
+FN:150,Transformation_freezeSitePosition
+FN:167,Transformation_unfreezeSitePosition
+FN:184,Transformation_slideSiteTo
+FN:191,finish
+FN:221,Transformation_rearrangeSites
+FN:227,Transformation_rearrangeSites/<
+FNDA:1,top-level
+FNDA:1,top-level
+FNDA:1,Page_init
+FNDA:2,update
+FNDA:1,Page_init
+FNDA:1,Page_init/<
+FNDA:1,Page_updateAttributes
+FNDA:1,Page_handleUnloadEvent
+FNDA:2,Page_handleEvent
+FNDA:1,gPage.onPageFirstVisible
+FNDA:1,onPageVisibleAndLoaded
+FNDA:1,reportLastVisibleTileIndex
+FNDA:2,gGrid.node
+FNDA:13,gGrid.cells
+FNDA:12,gGrid.sites
+FNDA:1,gGrid.ready
+FNDA:4,gGrid.isDocumentLoaded
+FNDA:1,Grid_init
+FNDA:1,Grid_init/<
+FNDA:2,Grid_createSite
+FNDA:1,Grid_handleEvent
+FNDA:1,refresh
+FNDA:2,_refreshGrid
+FNDA:4,Grid_computeHeight
+FNDA:1,Grid_createSiteFragment
+FNDA:8,Grid_isHistoricalTile
+FNDA:3,Grid_resizeGrid
+FNDA:30,Cell
+FNDA:120,Cell/<
+FNDA:183,Cell.prototype.node
+FNDA:180,Cell.prototype.site
+FNDA:2,Site
+FNDA:16,Site.prototype.node
+FNDA:33,Site.prototype.link
+FNDA:7,Site.prototype.url
+FNDA:4,Site.prototype.title
+FNDA:2,Site_isPinned
+FNDA:12,Site_querySelector
+FNDA:2,Site_checkLinkEndTime
+FNDA:2,Site_render
+FNDA:1,Site_onFirstVisible
+FNDA:3,Site_captureIfMissing
+FNDA:2,Site_refreshThumbnail
+FNDA:4,Site.prototype._ignoreHoverEvents
+FNDA:2,Site_addEventHandlers
+FNDA:1,gDropTargetShim.init
+FNDA:1,UndoDialog_init
+FNDA:1,gSearch.init
+FNDA:1,gCustomize.init
+FNDA:1,gCustomize.updateSelected
+FNDA:3,gCustomize.updateSelected/<
+FNDA:1,gIntro.init
+FNDA:1,gIntro.showIfNecessary
+FNDA:3,newTabString
+FNF:187
+FNH:54
+BRDA:233,0,0,-
+BRDA:233,0,1,-
+BRDA:236,1,0,-
+BRDA:236,1,1,-
+BRDA:263,0,0,-
+BRDA:263,0,1,-
+BRDA:289,0,0,-
+BRDA:289,0,1,-
+BRDA:290,1,0,-
+BRDA:290,1,1,-
+BRDA:293,2,0,-
+BRDA:293,2,1,-
+BRDA:348,0,0,-
+BRDA:348,0,1,1
+BRDA:364,0,0,-
+BRDA:364,0,1,-
+BRDA:371,1,0,-
+BRDA:371,1,1,-
+BRDA:377,2,0,-
+BRDA:377,2,1,-
+BRDA:382,3,0,-
+BRDA:382,3,1,-
+BRDA:382,4,0,-
+BRDA:382,4,1,-
+BRDA:384,5,0,-
+BRDA:384,5,1,-
+BRDA:384,6,0,-
+BRDA:384,6,1,-
+BRDA:383,7,0,-
+BRDA:383,7,1,-
+BRDA:399,0,0,2
+BRDA:399,0,1,-
+BRDA:401,1,0,-
+BRDA:401,1,1,2
+BRDA:405,2,0,1
+BRDA:405,2,1,1
+BRDA:405,3,0,1
+BRDA:405,3,1,1
+BRDA:413,4,0,-
+BRDA:413,4,1,-
+BRDA:419,0,0,-
+BRDA:419,0,1,-
+BRDA:432,0,0,1
+BRDA:432,0,1,-
+BRDA:440,1,0,1
+BRDA:440,1,1,-
+BRDA:463,0,0,-
+BRDA:463,0,1,2
+BRDA:462,1,0,2
+BRDA:462,1,1,1
+BRDA:472,2,0,-
+BRDA:472,2,1,-
+BRDA:471,3,0,-
+BRDA:471,3,1,1
+BRDA:488,0,0,1
+BRDA:488,0,1,-
+BRDA:500,0,0,1
+BRDA:500,0,1,1
+BRDA:500,1,0,1
+BRDA:500,1,1,-
+BRDA:500,2,0,-
+BRDA:500,2,1,-
+BRDA:500,3,0,-
+BRDA:500,3,1,-
+BRDA:500,4,0,-
+BRDA:500,4,1,-
+BRDA:500,5,0,-
+BRDA:500,5,1,-
+BRDA:511,6,0,-
+BRDA:511,6,1,-
+BRDA:510,7,0,-
+BRDA:510,7,1,-
+BRDA:519,8,0,-
+BRDA:519,8,1,-
+BRDA:519,9,0,-
+BRDA:519,9,1,-
+BRDA:523,10,0,-
+BRDA:523,10,1,-
+BRDA:523,11,0,-
+BRDA:523,11,1,-
+BRDA:530,12,0,-
+BRDA:530,12,1,-
+BRDA:549,0,0,14
+BRDA:549,0,1,1
+BRDA:548,1,0,16
+BRDA:548,1,1,1
+BRDA:560,2,0,1
+BRDA:560,2,1,-
+BRDA:588,0,0,1
+BRDA:588,0,1,22
+BRDA:588,1,0,15
+BRDA:588,1,1,8
+BRDA:589,2,0,7
+BRDA:589,2,1,1
+BRDA:591,3,0,1
+BRDA:591,3,1,-
+BRDA:587,4,0,24
+BRDA:587,4,1,1
+BRDA:635,0,0,181
+BRDA:635,0,1,12
+BRDA:665,0,0,-
+BRDA:665,0,1,1
+BRDA:686,0,0,1
+BRDA:686,0,1,-
+BRDA:686,1,0,-
+BRDA:686,1,1,-
+BRDA:728,0,0,31
+BRDA:728,0,1,2
+BRDA:733,1,0,30
+BRDA:733,1,1,2
+BRDA:741,2,0,-
+BRDA:741,2,1,2
+BRDA:740,3,0,2
+BRDA:740,3,1,2
+BRDA:757,0,0,2
+BRDA:757,0,1,2
+BRDA:793,0,0,8
+BRDA:793,0,1,-
+BRDA:793,1,0,-
+BRDA:793,1,1,-
+BRDA:793,2,0,-
+BRDA:793,2,1,-
+BRDA:804,0,0,1
+BRDA:804,0,1,2
+BRDA:804,1,0,2
+BRDA:804,1,1,1
+BRDA:809,2,0,1
+BRDA:809,2,1,1
+BRDA:819,3,0,1
+BRDA:819,3,1,1
+BRDA:839,4,0,8
+BRDA:839,4,1,-
+BRDA:838,5,0,9
+BRDA:838,5,1,2
+BRDA:909,0,0,-
+BRDA:909,0,1,-
+BRDA:922,0,0,-
+BRDA:922,0,1,-
+BRDA:935,0,0,168
+BRDA:935,0,1,12
+BRDA:944,0,0,-
+BRDA:944,0,1,-
+BRDA:961,0,0,-
+BRDA:961,0,1,-
+BRDA:961,1,0,-
+BRDA:961,1,1,-
+BRDA:964,2,0,-
+BRDA:964,2,1,-
+BRDA:964,3,0,-
+BRDA:964,3,1,-
+BRDA:967,4,0,-
+BRDA:967,4,1,-
+BRDA:967,5,0,-
+BRDA:967,5,1,-
+BRDA:967,6,0,-
+BRDA:967,6,1,-
+BRDA:967,7,0,-
+BRDA:967,7,1,-
+BRDA:1020,0,0,4
+BRDA:1020,0,1,-
+BRDA:1027,0,0,-
+BRDA:1027,0,1,-
+BRDA:1036,0,0,-
+BRDA:1036,0,1,-
+BRDA:1041,1,0,-
+BRDA:1041,1,1,-
+BRDA:1052,0,0,-
+BRDA:1052,0,1,-
+BRDA:1072,0,0,-
+BRDA:1072,0,1,-
+BRDA:1096,0,0,-
+BRDA:1096,0,1,-
+BRDA:1108,0,0,-
+BRDA:1108,0,1,-
+BRDA:1119,0,0,-
+BRDA:1119,0,1,-
+BRDA:1129,0,0,2
+BRDA:1129,0,1,-
+BRDA:1129,1,0,2
+BRDA:1129,1,1,-
+BRDA:1151,0,0,-
+BRDA:1151,0,1,2
+BRDA:1153,1,0,-
+BRDA:1153,1,1,2
+BRDA:1153,2,0,-
+BRDA:1153,2,1,2
+BRDA:1154,3,0,-
+BRDA:1154,3,1,-
+BRDA:1156,4,0,2
+BRDA:1156,4,1,-
+BRDA:1165,5,0,-
+BRDA:1165,5,1,2
+BRDA:1173,6,0,2
+BRDA:1173,6,1,-
+BRDA:1174,7,0,-
+BRDA:1174,7,1,-
+BRDA:1185,8,0,2
+BRDA:1185,8,1,-
+BRDA:1200,0,0,1
+BRDA:1200,0,1,-
+BRDA:1200,1,0,1
+BRDA:1200,1,1,-
+BRDA:1214,0,0,-
+BRDA:1214,0,1,3
+BRDA:1214,1,0,-
+BRDA:1214,1,1,3
+BRDA:1224,0,0,-
+BRDA:1224,0,1,2
+BRDA:1224,1,0,2
+BRDA:1224,1,1,-
+BRDA:1228,2,0,2
+BRDA:1228,2,1,-
+BRDA:1232,3,0,-
+BRDA:1232,3,1,2
+BRDA:1235,4,0,-
+BRDA:1235,4,1,2
+BRDA:1239,5,0,2
+BRDA:1239,5,1,-
+BRDA:1285,0,0,-
+BRDA:1285,0,1,-
+BRDA:1286,1,0,-
+BRDA:1286,1,1,-
+BRDA:1287,2,0,-
+BRDA:1287,2,1,-
+BRDA:1298,0,0,-
+BRDA:1298,0,1,-
+BRDA:1311,1,0,-
+BRDA:1311,1,1,-
+BRDA:1311,2,0,-
+BRDA:1311,2,1,-
+BRDA:1314,3,0,-
+BRDA:1314,3,1,-
+BRDA:1315,4,0,-
+BRDA:1315,4,1,-
+BRDA:1331,0,0,-
+BRDA:1331,0,1,-
+BRDA:1332,1,0,-
+BRDA:1332,1,1,-
+BRDA:1334,2,0,-
+BRDA:1334,2,1,-
+BRDA:1334,3,0,-
+BRDA:1334,3,1,-
+BRDA:1340,4,0,-
+BRDA:1340,4,1,-
+BRDA:1343,5,0,-
+BRDA:1343,5,1,-
+BRDA:1347,6,0,-
+BRDA:1347,6,1,-
+BRDA:1349,7,0,-
+BRDA:1349,7,1,-
+BRDA:1353,8,0,-
+BRDA:1353,8,1,-
+BRDA:1359,9,0,-
+BRDA:1359,9,1,-
+BRDA:1360,10,0,-
+BRDA:1360,10,1,-
+BRDA:1364,11,0,-
+BRDA:1364,11,1,-
+BRDA:1364,12,0,-
+BRDA:1364,12,1,-
+BRDA:1368,13,0,-
+BRDA:1368,13,1,-
+BRDA:1368,14,0,-
+BRDA:1368,14,1,-
+BRDA:1369,15,0,-
+BRDA:1369,15,1,-
+BRDA:1378,16,0,-
+BRDA:1378,16,1,-
+BRDA:1387,0,0,-
+BRDA:1387,0,1,-
+BRDA:1387,1,0,-
+BRDA:1387,1,1,-
+BRDA:1387,2,0,-
+BRDA:1387,2,1,-
+BRDA:1439,0,0,-
+BRDA:1439,0,1,-
+BRDA:1491,0,0,-
+BRDA:1491,0,1,-
+BRDA:1510,0,0,-
+BRDA:1510,0,1,-
+BRDA:1510,1,0,-
+BRDA:1510,1,1,-
+BRDA:1557,0,0,-
+BRDA:1557,0,1,-
+BRDA:1557,1,0,-
+BRDA:1557,1,1,-
+BRDA:1561,2,0,-
+BRDA:1561,2,1,-
+BRDA:1562,3,0,-
+BRDA:1562,3,1,-
+BRDA:1562,4,0,-
+BRDA:1562,4,1,-
+BRDA:1595,0,0,-
+BRDA:1595,0,1,-
+BRDA:1595,1,0,-
+BRDA:1595,1,1,-
+BRDA:1612,0,0,-
+BRDA:1612,0,1,-
+BRDA:1633,0,0,-
+BRDA:1633,0,1,-
+BRDA:1649,0,0,-
+BRDA:1649,0,1,-
+BRDA:1651,1,0,-
+BRDA:1651,1,1,-
+BRDA:1655,2,0,-
+BRDA:1655,2,1,-
+BRDA:1671,0,0,-
+BRDA:1671,0,1,-
+BRDA:1692,0,0,-
+BRDA:1692,0,1,-
+BRDA:1706,0,0,-
+BRDA:1706,0,1,-
+BRDA:1765,0,0,-
+BRDA:1765,0,1,-
+BRDA:1765,1,0,-
+BRDA:1765,1,1,-
+BRDA:1765,2,0,-
+BRDA:1765,2,1,-
+BRDA:1765,3,0,-
+BRDA:1765,3,1,-
+BRDA:1765,4,0,-
+BRDA:1765,4,1,-
+BRDA:1789,0,0,-
+BRDA:1789,0,1,-
+BRDA:1810,0,0,-
+BRDA:1810,0,1,-
+BRDA:1840,0,0,-
+BRDA:1840,0,1,-
+BRDA:1841,1,0,-
+BRDA:1841,1,1,-
+BRDA:1841,2,0,-
+BRDA:1841,2,1,-
+BRDA:1866,0,0,-
+BRDA:1866,0,1,-
+BRDA:1867,1,0,-
+BRDA:1867,1,1,-
+BRDA:1871,2,0,-
+BRDA:1871,2,1,-
+BRDA:1875,3,0,-
+BRDA:1875,3,1,-
+BRDA:1902,0,0,-
+BRDA:1902,0,1,-
+BRDA:1902,1,0,-
+BRDA:1902,1,1,-
+BRDA:1898,2,0,-
+BRDA:1898,2,1,-
+BRDA:1915,0,0,-
+BRDA:1915,0,1,-
+BRDA:1977,0,0,-
+BRDA:1977,0,1,-
+BRDA:1982,1,0,-
+BRDA:1982,1,1,-
+BRDA:2012,0,0,-
+BRDA:2012,0,1,-
+BRDA:2032,0,0,-
+BRDA:2032,0,1,-
+BRDA:2032,1,0,-
+BRDA:2032,1,1,-
+BRDA:2032,2,0,-
+BRDA:2032,2,1,-
+BRDA:2038,3,0,-
+BRDA:2038,3,1,-
+BRDA:2052,0,0,-
+BRDA:2052,0,1,-
+BRDA:2056,1,0,-
+BRDA:2056,1,1,-
+BRDA:2056,2,0,-
+BRDA:2056,2,1,-
+BRDA:2062,3,0,-
+BRDA:2062,3,1,-
+BRDA:2062,4,0,-
+BRDA:2062,4,1,-
+BRDA:2081,0,0,-
+BRDA:2081,0,1,-
+BRDA:2087,1,0,-
+BRDA:2087,1,1,-
+BRDA:2093,2,0,-
+BRDA:2093,2,1,-
+BRDA:2109,0,0,-
+BRDA:2109,0,1,-
+BRDA:2116,1,0,-
+BRDA:2116,1,1,-
+BRDA:2115,2,0,-
+BRDA:2115,2,1,-
+BRDA:2145,0,0,-
+BRDA:2145,0,1,-
+BRDA:2151,1,0,-
+BRDA:2151,1,1,-
+BRDA:2151,2,0,-
+BRDA:2151,2,1,-
+BRDA:2143,3,0,-
+BRDA:2143,3,1,-
+BRDA:2211,0,0,-
+BRDA:2211,0,1,-
+BRDA:2217,0,0,-
+BRDA:2217,0,1,-
+BRDA:2217,1,0,-
+BRDA:2217,1,1,-
+BRDA:2227,0,0,-
+BRDA:2227,0,1,-
+BRDA:2249,0,0,-
+BRDA:2249,0,1,-
+BRDA:2249,1,0,-
+BRDA:2249,1,1,-
+BRDA:2253,2,0,-
+BRDA:2253,2,1,-
+BRDA:2257,3,0,-
+BRDA:2257,3,1,-
+BRDA:2285,0,0,-
+BRDA:2285,0,1,-
+BRDA:2285,1,0,-
+BRDA:2285,1,1,-
+BRDA:2313,0,0,-
+BRDA:2313,0,1,-
+BRDA:2313,1,0,-
+BRDA:2313,1,1,-
+BRDA:2364,0,0,-
+BRDA:2364,0,1,-
+BRDA:2384,0,0,-
+BRDA:2384,0,1,-
+BRDA:2400,0,0,-
+BRDA:2400,0,1,-
+BRDA:2400,1,0,-
+BRDA:2400,1,1,-
+BRDA:2400,2,0,-
+BRDA:2400,2,1,-
+BRDA:2417,0,0,-
+BRDA:2417,0,1,-
+BRDA:2423,1,0,-
+BRDA:2423,1,1,-
+BRDA:2470,0,0,7
+BRDA:2470,0,1,1
+BRDA:2496,0,0,-
+BRDA:2496,0,1,-
+BRDA:2518,0,0,-
+BRDA:2518,0,1,-
+BRDA:2518,1,0,-
+BRDA:2518,1,1,-
+BRDA:2529,0,0,-
+BRDA:2529,0,1,-
+BRDA:2530,1,0,-
+BRDA:2530,1,1,-
+BRDA:2534,2,0,-
+BRDA:2534,2,1,-
+BRDA:2534,3,0,-
+BRDA:2534,3,1,-
+BRDA:2534,4,0,-
+BRDA:2534,4,1,-
+BRDA:2534,5,0,-
+BRDA:2534,5,1,-
+BRDA:2539,6,0,-
+BRDA:2539,6,1,-
+BRDA:2555,0,0,-
+BRDA:2555,0,1,-
+BRDA:2567,0,0,-
+BRDA:2567,0,1,1
+BRDA:2567,1,0,-
+BRDA:2567,1,1,1
+BRDA:2577,2,0,-
+BRDA:2577,2,1,1
+BRDA:2570,0,0,2
+BRDA:2570,0,1,1
+BRDA:2603,0,0,6
+BRDA:2603,0,1,1
+BRDA:2647,0,0,1
+BRDA:2647,0,1,-
+BRDA:2650,1,0,1
+BRDA:2650,1,1,-
+BRDA:2660,0,0,-
+BRDA:2660,0,1,-
+BRDA:38,0,0,-
+BRDA:38,0,1,3
+BRDA:101,0,0,-
+BRDA:101,0,1,-
+BRDA:151,0,0,-
+BRDA:151,0,1,-
+BRDA:168,0,0,-
+BRDA:168,0,1,-
+BRDA:187,0,0,-
+BRDA:187,0,1,-
+BRDA:204,1,0,-
+BRDA:204,1,1,-
+BRDA:205,2,0,-
+BRDA:205,2,1,-
+BRDA:192,0,0,-
+BRDA:192,0,1,-
+BRDA:192,1,0,-
+BRDA:192,1,1,-
+BRDA:195,2,0,-
+BRDA:195,2,1,-
+BRDA:224,0,0,-
+BRDA:224,0,1,-
+BRDA:225,1,0,-
+BRDA:225,1,1,-
+BRDA:246,2,0,-
+BRDA:246,2,1,-
+BRDA:229,0,0,-
+BRDA:229,0,1,-
+BRDA:229,1,0,-
+BRDA:229,1,1,-
+BRF:500
+BRH:90
+DA:7,1
+DA:8,1
+DA:23,1
+DA:24,1
+DA:25,1
+DA:26,1
+DA:27,1
+DA:28,1
+DA:36,1
+DA:48,1
+DA:49,1
+DA:51,1
+DA:52,1
+DA:53,1
+DA:62,1
+DA:324,1
+DA:330,1
+DA:607,1
+DA:608,1
+DA:609,1
+DA:614,1
+DA:870,1
+DA:1406,1
+DA:1550,1
+DA:1570,1
+DA:1575,1
+DA:1719,1
+DA:1947,1
+DA:2164,1
+DA:2337,1
+DA:2445,1
+DA:2456,1
+DA:2585,1
+DA:2586,1
+DA:2588,1
+DA:7,1
+DA:8,1
+DA:10,1
+DA:11,1
+DA:12,1
+DA:13,1
+DA:14,1
+DA:15,1
+DA:17,1
+DA:18,1
+DA:17,1
+DA:19,1
+DA:20,1
+DA:19,1
+DA:29,1
+DA:31,1
+DA:48,1
+DA:49,1
+DA:51,1
+DA:52,1
+DA:53,1
+DA:62,1
+DA:67,1
+DA:86,1
+DA:96,1
+DA:111,1
+DA:120,1
+DA:129,1
+DA:138,1
+DA:150,1
+DA:167,1
+DA:184,1
+DA:221,1
+DA:259,1
+DA:275,1
+DA:287,1
+DA:307,1
+DA:317,1
+DA:324,1
+DA:330,1
+DA:334,1
+DA:363,1
+DA:399,1
+DA:431,1
+DA:459,1
+DA:482,1
+DA:499,1
+DA:544,1
+DA:567,1
+DA:575,1
+DA:607,1
+DA:608,1
+DA:609,1
+DA:614,1
+DA:618,1
+DA:619,1
+DA:624,1
+DA:629,1
+DA:630,1
+DA:635,1
+DA:638,1
+DA:641,1
+DA:647,1
+DA:676,1
+DA:685,1
+DA:697,1
+DA:704,1
+DA:714,1
+DA:722,1
+DA:755,1
+DA:764,1
+DA:791,1
+DA:799,1
+DA:881,1
+DA:885,1
+DA:890,1
+DA:895,1
+DA:907,1
+DA:920,1
+DA:933,1
+DA:942,1
+DA:951,1
+DA:958,1
+DA:1001,1
+DA:1005,1
+DA:1010,1
+DA:1015,1
+DA:1020,1
+DA:1025,1
+DA:1035,1
+DA:1051,1
+DA:1063,1
+DA:1071,1
+DA:1084,1
+DA:1093,1
+DA:1105,1
+DA:1116,1
+DA:1128,1
+DA:1147,1
+DA:1199,1
+DA:1213,1
+DA:1222,1
+DA:1246,1
+DA:1258,1
+DA:1275,1
+DA:1284,1
+DA:1296,1
+DA:1324,1
+DA:1386,1
+DA:1406,1
+DA:1410,1
+DA:1411,1
+DA:1416,1
+DA:1417,1
+DA:1422,1
+DA:1423,1
+DA:1424,1
+DA:1425,1
+DA:1432,1
+DA:1465,1
+DA:1489,1
+DA:1505,1
+DA:1524,1
+DA:1550,1
+DA:1551,1
+DA:1555,1
+DA:1570,1
+DA:1575,1
+DA:1579,1
+DA:1585,1
+DA:1594,1
+DA:1609,1
+DA:1628,1
+DA:1645,1
+DA:1669,1
+DA:1691,1
+DA:1702,1
+DA:1719,1
+DA:1723,1
+DA:1728,1
+DA:1733,1
+DA:1740,1
+DA:1752,1
+DA:1764,1
+DA:1788,1
+DA:1799,1
+DA:1819,1
+DA:1839,1
+DA:1862,1
+DA:1888,1
+DA:1914,1
+DA:1929,1
+DA:1947,1
+DA:1954,1
+DA:1972,1
+DA:1999,1
+DA:2023,1
+DA:2047,1
+DA:2077,1
+DA:2104,1
+DA:2135,1
+DA:2164,1
+DA:2170,1
+DA:2206,1
+DA:2225,1
+DA:2236,1
+DA:2268,1
+DA:2279,1
+DA:2308,1
+DA:2337,1
+DA:2341,1
+DA:2346,1
+DA:2351,1
+DA:2363,1
+DA:2383,1
+DA:2399,1
+DA:2416,1
+DA:2434,1
+DA:2442,1
+DA:2445,1
+DA:2446,1
+DA:2456,1
+DA:2457,1
+DA:2467,1
+DA:2469,1
+DA:2483,1
+DA:2495,1
+DA:2517,1
+DA:2528,1
+DA:2554,1
+DA:2560,1
+DA:2565,1
+DA:2585,1
+DA:2586,1
+DA:2588,1
+DA:2589,1
+DA:2598,1
+DA:2600,1
+DA:2602,1
+DA:2608,1
+DA:2621,1
+DA:2625,1
+DA:2629,1
+DA:2636,1
+DA:2646,1
+DA:2656,1
+DA:2677,1
+DA:32,1
+DA:33,1
+DA:32,1
+DA:233,0
+DA:235,0
+DA:236,0
+DA:238,0
+DA:241,0
+DA:261,0
+DA:262,0
+DA:263,0
+DA:264,0
+DA:265,0
+DA:276,0
+DA:277,0
+DA:289,0
+DA:290,0
+DA:291,0
+DA:293,0
+DA:294,0
+DA:297,0
+DA:308,0
+DA:309,0
+DA:318,0
+DA:336,1
+DA:339,1
+DA:344,1
+DA:347,1
+DA:348,1
+DA:349,1
+DA:351,1
+DA:354,1
+DA:357,1
+DA:364,0
+DA:365,0
+DA:367,0
+DA:368,0
+DA:371,0
+DA:372,0
+DA:373,0
+DA:377,0
+DA:378,0
+DA:380,0
+DA:382,0
+DA:383,0
+DA:384,0
+DA:385,0
+DA:383,0
+DA:401,2
+DA:405,2
+DA:406,1
+DA:409,2
+DA:413,0
+DA:414,0
+DA:417,0
+DA:424,0
+DA:417,0
+DA:419,0
+DA:420,0
+DA:423,0
+DA:432,1
+DA:433,0
+DA:435,1
+DA:438,1
+DA:440,1
+DA:441,0
+DA:443,1
+DA:447,1
+DA:450,1
+DA:461,1
+DA:462,1
+DA:463,2
+DA:464,2
+DA:466,0
+DA:462,3
+DA:470,1
+DA:471,1
+DA:472,0
+DA:473,0
+DA:475,0
+DA:471,1
+DA:483,1
+DA:487,1
+DA:488,1
+DA:489,0
+DA:492,1
+DA:500,2
+DA:502,1
+DA:505,1
+DA:508,0
+DA:511,0
+DA:512,0
+DA:515,0
+DA:510,0
+DA:519,0
+DA:520,0
+DA:523,0
+DA:524,0
+DA:525,0
+DA:530,0
+DA:531,0
+DA:532,0
+DA:535,0
+DA:538,0
+DA:539,0
+DA:546,1
+DA:548,1
+DA:549,15
+DA:553,1
+DA:548,17
+DA:558,1
+DA:560,1
+DA:561,0
+DA:563,1
+DA:569,1
+DA:572,1
+DA:576,1
+DA:577,1
+DA:576,1
+DA:579,1
+DA:580,1
+DA:581,1
+DA:580,1
+DA:583,1
+DA:584,1
+DA:585,1
+DA:587,1
+DA:588,23
+DA:589,8
+DA:590,1
+DA:591,1
+DA:593,0
+DA:587,25
+DA:599,1
+DA:648,1
+DA:649,1
+DA:651,1
+DA:665,1
+DA:666,1
+DA:652,1
+DA:653,1
+DA:659,1
+DA:661,1
+DA:677,2
+DA:678,2
+DA:679,2
+DA:686,1
+DA:689,1
+DA:698,0
+DA:705,0
+DA:715,1
+DA:716,1
+DA:723,2
+DA:724,2
+DA:727,2
+DA:728,2
+DA:729,30
+DA:728,30
+DA:733,2
+DA:736,2
+DA:739,2
+DA:740,2
+DA:741,2
+DA:742,2
+DA:740,2
+DA:746,2
+DA:747,2
+DA:748,2
+DA:756,4
+DA:757,4
+DA:758,4
+DA:765,1
+DA:766,1
+DA:767,1
+DA:770,1
+DA:771,1
+DA:777,1
+DA:779,1
+DA:783,1
+DA:784,1
+DA:792,8
+DA:793,8
+DA:804,3
+DA:805,1
+DA:809,2
+DA:810,1
+DA:811,1
+DA:812,1
+DA:813,1
+DA:814,1
+DA:817,2
+DA:819,2
+DA:820,1
+DA:821,1
+DA:825,2
+DA:826,2
+DA:827,2
+DA:830,2
+DA:832,2
+DA:833,2
+DA:832,2
+DA:835,2
+DA:837,2
+DA:839,8
+DA:842,8
+DA:838,11
+DA:849,2
+DA:856,2
+DA:857,2
+DA:858,2
+DA:859,2
+DA:860,2
+DA:871,30
+DA:872,30
+DA:873,30
+DA:876,30
+DA:878,30
+DA:876,30
+DA:877,120
+DA:896,0
+DA:899,0
+DA:901,0
+DA:908,0
+DA:909,0
+DA:912,0
+DA:914,0
+DA:921,0
+DA:922,0
+DA:925,0
+DA:927,0
+DA:934,180
+DA:935,180
+DA:943,0
+DA:944,0
+DA:952,0
+DA:961,0
+DA:962,0
+DA:964,0
+DA:965,0
+DA:967,0
+DA:969,0
+DA:970,0
+DA:973,0
+DA:976,0
+DA:979,0
+DA:980,0
+DA:992,2
+DA:993,2
+DA:995,2
+DA:997,2
+DA:998,2
+DA:1026,0
+DA:1027,0
+DA:1036,0
+DA:1037,0
+DA:1039,0
+DA:1040,0
+DA:1041,0
+DA:1043,0
+DA:1045,0
+DA:1052,0
+DA:1053,0
+DA:1054,0
+DA:1055,0
+DA:1064,2
+DA:1072,0
+DA:1073,0
+DA:1074,0
+DA:1075,0
+DA:1085,12
+DA:1094,0
+DA:1096,0
+DA:1097,0
+DA:1098,0
+DA:1100,0
+DA:1101,0
+DA:1106,0
+DA:1107,0
+DA:1109,0
+DA:1110,0
+DA:1111,0
+DA:1108,0
+DA:1113,0
+DA:1117,0
+DA:1118,0
+DA:1119,0
+DA:1120,0
+DA:1122,0
+DA:1129,2
+DA:1130,0
+DA:1132,0
+DA:1134,0
+DA:1135,0
+DA:1137,0
+DA:1139,0
+DA:1140,0
+DA:1149,2
+DA:1151,2
+DA:1152,2
+DA:1153,2
+DA:1154,0
+DA:1155,0
+DA:1156,2
+DA:1158,2
+DA:1159,2
+DA:1160,2
+DA:1161,2
+DA:1163,2
+DA:1164,2
+DA:1165,2
+DA:1166,2
+DA:1171,2
+DA:1173,2
+DA:1174,0
+DA:1175,0
+DA:1176,0
+DA:1179,0
+DA:1180,0
+DA:1181,0
+DA:1182,0
+DA:1185,2
+DA:1186,0
+DA:1189,2
+DA:1191,2
+DA:1200,1
+DA:1202,0
+DA:1205,1
+DA:1214,3
+DA:1215,3
+DA:1224,2
+DA:1225,0
+DA:1227,2
+DA:1228,2
+DA:1229,0
+DA:1232,2
+DA:1233,2
+DA:1235,2
+DA:1236,2
+DA:1237,2
+DA:1239,2
+DA:1240,0
+DA:1241,0
+DA:1247,4
+DA:1250,4
+DA:1248,0
+DA:1251,0
+DA:1260,2
+DA:1261,2
+DA:1262,2
+DA:1266,2
+DA:1267,2
+DA:1268,2
+DA:1269,2
+DA:1276,0
+DA:1277,0
+DA:1278,0
+DA:1285,0
+DA:1286,0
+DA:1287,0
+DA:1290,0
+DA:1292,0
+DA:1293,0
+DA:1292,0
+DA:1297,0
+DA:1298,0
+DA:1299,0
+DA:1300,0
+DA:1302,0
+DA:1304,0
+DA:1305,0
+DA:1306,0
+DA:1307,0
+DA:1309,0
+DA:1310,0
+DA:1311,0
+DA:1312,0
+DA:1313,0
+DA:1314,0
+DA:1315,0
+DA:1317,0
+DA:1325,0
+DA:1326,0
+DA:1327,0
+DA:1328,0
+DA:1331,0
+DA:1332,0
+DA:1334,0
+DA:1335,0
+DA:1336,0
+DA:1340,0
+DA:1341,0
+DA:1343,0
+DA:1344,0
+DA:1347,0
+DA:1348,0
+DA:1349,0
+DA:1353,0
+DA:1354,0
+DA:1356,0
+DA:1357,0
+DA:1359,0
+DA:1360,0
+DA:1361,0
+DA:1362,0
+DA:1364,0
+DA:1365,0
+DA:1366,0
+DA:1368,0
+DA:1369,0
+DA:1371,0
+DA:1373,0
+DA:1378,0
+DA:1379,0
+DA:1387,0
+DA:1389,0
+DA:1390,0
+DA:1393,0
+DA:1396,0
+DA:1433,0
+DA:1436,0
+DA:1437,0
+DA:1438,0
+DA:1439,0
+DA:1440,0
+DA:1439,0
+DA:1442,0
+DA:1444,0
+DA:1447,0
+DA:1448,0
+DA:1449,0
+DA:1450,0
+DA:1453,0
+DA:1454,0
+DA:1455,0
+DA:1457,0
+DA:1467,0
+DA:1470,0
+DA:1473,0
+DA:1474,0
+DA:1477,0
+DA:1478,0
+DA:1481,0
+DA:1490,0
+DA:1491,0
+DA:1492,0
+DA:1491,0
+DA:1495,0
+DA:1497,0
+DA:1506,0
+DA:1510,0
+DA:1511,0
+DA:1516,0
+DA:1525,0
+DA:1527,0
+DA:1528,0
+DA:1529,0
+DA:1530,0
+DA:1531,0
+DA:1532,0
+DA:1533,0
+DA:1537,0
+DA:1538,0
+DA:1539,0
+DA:1540,0
+DA:1541,0
+DA:1545,0
+DA:1552,0
+DA:1556,0
+DA:1557,0
+DA:1558,0
+DA:1561,0
+DA:1562,0
+DA:1563,0
+DA:1586,0
+DA:1595,0
+DA:1596,0
+DA:1599,0
+DA:1600,0
+DA:1612,0
+DA:1613,0
+DA:1616,0
+DA:1618,0
+DA:1621,0
+DA:1629,0
+DA:1632,0
+DA:1637,0
+DA:1633,0
+DA:1646,0
+DA:1647,0
+DA:1649,0
+DA:1651,0
+DA:1652,0
+DA:1653,0
+DA:1654,0
+DA:1655,0
+DA:1657,0
+DA:1660,0
+DA:1676,0
+DA:1671,0
+DA:1672,0
+DA:1674,0
+DA:1681,0
+DA:1682,0
+DA:1685,0
+DA:1677,0
+DA:1678,0
+DA:1692,0
+DA:1693,0
+DA:1694,0
+DA:1703,0
+DA:1706,0
+DA:1707,0
+DA:1709,0
+DA:1734,1
+DA:1741,0
+DA:1743,0
+DA:1744,0
+DA:1745,0
+DA:1746,0
+DA:1753,0
+DA:1755,0
+DA:1756,0
+DA:1757,0
+DA:1758,0
+DA:1765,0
+DA:1767,0
+DA:1770,0
+DA:1773,0
+DA:1776,0
+DA:1779,0
+DA:1789,0
+DA:1790,0
+DA:1791,0
+DA:1802,0
+DA:1803,0
+DA:1806,0
+DA:1810,0
+DA:1811,0
+DA:1821,0
+DA:1825,0
+DA:1829,0
+DA:1832,0
+DA:1840,0
+DA:1841,0
+DA:1843,0
+DA:1844,0
+DA:1848,0
+DA:1849,0
+DA:1852,0
+DA:1853,0
+DA:1854,0
+DA:1864,0
+DA:1866,0
+DA:1867,0
+DA:1869,0
+DA:1871,0
+DA:1873,0
+DA:1875,0
+DA:1877,0
+DA:1879,0
+DA:1891,0
+DA:1892,0
+DA:1894,0
+DA:1895,0
+DA:1898,0
+DA:1899,0
+DA:1902,0
+DA:1903,0
+DA:1898,0
+DA:1907,0
+DA:1915,0
+DA:1916,0
+DA:1918,0
+DA:1919,0
+DA:1930,0
+DA:1931,0
+DA:1934,0
+DA:1935,0
+DA:1934,0
+DA:1937,0
+DA:1955,0
+DA:1958,0
+DA:1962,0
+DA:1964,0
+DA:1973,0
+DA:1974,0
+DA:1977,0
+DA:1978,0
+DA:1979,0
+DA:1982,0
+DA:1983,0
+DA:1984,0
+DA:1988,0
+DA:2002,0
+DA:2005,0
+DA:2008,0
+DA:2005,0
+DA:2012,0
+DA:2013,0
+DA:2006,0
+DA:2007,0
+DA:2024,0
+DA:2028,0
+DA:2030,0
+DA:2032,0
+DA:2033,0
+DA:2035,0
+DA:2038,0
+DA:2048,0
+DA:2049,0
+DA:2052,0
+DA:2053,0
+DA:2057,0
+DA:2056,0
+DA:2059,0
+DA:2063,0
+DA:2062,0
+DA:2066,0
+DA:2081,0
+DA:2082,0
+DA:2084,0
+DA:2087,0
+DA:2088,0
+DA:2090,0
+DA:2093,0
+DA:2107,0
+DA:2109,0
+DA:2110,0
+DA:2111,0
+DA:2115,0
+DA:2116,0
+DA:2117,0
+DA:2118,0
+DA:2115,0
+DA:2123,0
+DA:2137,0
+DA:2138,0
+DA:2143,0
+DA:2145,0
+DA:2146,0
+DA:2148,0
+DA:2151,0
+DA:2152,0
+DA:2143,0
+DA:2155,0
+DA:2171,0
+DA:2174,0
+DA:2177,0
+DA:2180,0
+DA:2185,0
+DA:2189,0
+DA:2191,0
+DA:2194,0
+DA:2207,0
+DA:2210,0
+DA:2216,0
+DA:2211,0
+DA:2212,0
+DA:2217,0
+DA:2226,0
+DA:2227,0
+DA:2228,0
+DA:2237,0
+DA:2242,0
+DA:2244,0
+DA:2260,0
+DA:2244,0
+DA:2245,0
+DA:2246,0
+DA:2249,0
+DA:2250,0
+DA:2253,0
+DA:2254,0
+DA:2257,0
+DA:2258,0
+DA:2269,0
+DA:2270,0
+DA:2280,0
+DA:2283,0
+DA:2300,0
+DA:2285,0
+DA:2286,0
+DA:2288,0
+DA:2290,0
+DA:2291,0
+DA:2294,0
+DA:2295,0
+DA:2309,0
+DA:2312,0
+DA:2328,0
+DA:2312,0
+DA:2328,0
+DA:2312,0
+DA:2313,0
+DA:2314,0
+DA:2316,0
+DA:2318,0
+DA:2321,0
+DA:2325,0
+DA:2326,0
+DA:2352,1
+DA:2353,1
+DA:2354,1
+DA:2355,1
+DA:2356,1
+DA:2364,0
+DA:2365,0
+DA:2367,0
+DA:2368,0
+DA:2369,0
+DA:2370,0
+DA:2371,0
+DA:2374,0
+DA:2375,0
+DA:2376,0
+DA:2377,0
+DA:2384,0
+DA:2385,0
+DA:2387,0
+DA:2388,0
+DA:2389,0
+DA:2390,0
+DA:2391,0
+DA:2392,0
+DA:2400,0
+DA:2402,0
+DA:2405,0
+DA:2408,0
+DA:2417,0
+DA:2418,0
+DA:2420,0
+DA:2421,0
+DA:2423,0
+DA:2424,0
+DA:2427,0
+DA:2428,0
+DA:2435,0
+DA:2438,0
+DA:2435,0
+DA:2436,0
+DA:2437,0
+DA:2447,1
+DA:2448,1
+DA:2447,1
+DA:2449,1
+DA:2450,1
+DA:2451,1
+DA:2470,1
+DA:2471,7
+DA:2470,8
+DA:2474,1
+DA:2475,1
+DA:2476,1
+DA:2477,1
+DA:2478,1
+DA:2480,1
+DA:2484,0
+DA:2488,0
+DA:2489,0
+DA:2490,0
+DA:2491,0
+DA:2492,0
+DA:2485,0
+DA:2486,0
+DA:2496,0
+DA:2497,0
+DA:2500,0
+DA:2501,0
+DA:2502,0
+DA:2503,0
+DA:2504,0
+DA:2507,0
+DA:2504,0
+DA:2509,0
+DA:2510,0
+DA:2514,0
+DA:2506,0
+DA:2518,0
+DA:2520,0
+DA:2523,0
+DA:2529,0
+DA:2530,0
+DA:2531,0
+DA:2534,0
+DA:2536,0
+DA:2539,0
+DA:2540,0
+DA:2542,0
+DA:2546,0
+DA:2549,0
+DA:2555,0
+DA:2556,0
+DA:2561,0
+DA:2562,0
+DA:2566,1
+DA:2567,1
+DA:2568,1
+DA:2577,1
+DA:2579,1
+DA:2569,3
+DA:2570,3
+DA:2571,1
+DA:2574,2
+DA:2603,1
+DA:2604,6
+DA:2603,7
+DA:2610,0
+DA:2612,0
+DA:2617,0
+DA:2618,0
+DA:2617,0
+DA:2613,0
+DA:2622,0
+DA:2626,0
+DA:2630,0
+DA:2631,0
+DA:2632,0
+DA:2637,0
+DA:2638,0
+DA:2639,0
+DA:2640,0
+DA:2641,0
+DA:2642,0
+DA:2639,0
+DA:2647,1
+DA:2648,0
+DA:2650,1
+DA:2651,0
+DA:2652,0
+DA:2657,0
+DA:2658,0
+DA:2660,0
+DA:2662,0
+DA:2664,0
+DA:2667,0
+DA:2670,0
+DA:2671,0
+DA:37,3
+DA:38,3
+DA:39,3
+DA:41,0
+DA:45,0
+DA:68,0
+DA:69,0
+DA:70,0
+DA:71,0
+DA:75,0
+DA:76,0
+DA:75,0
+DA:78,0
+DA:87,0
+DA:88,0
+DA:97,0
+DA:99,0
+DA:101,0
+DA:102,0
+DA:112,0
+DA:121,0
+DA:130,0
+DA:139,0
+DA:140,0
+DA:142,0
+DA:143,0
+DA:151,0
+DA:152,0
+DA:154,0
+DA:155,0
+DA:156,0
+DA:157,0
+DA:159,0
+DA:160,0
+DA:168,0
+DA:169,0
+DA:171,0
+DA:172,0
+DA:173,0
+DA:191,0
+DA:185,0
+DA:186,0
+DA:187,0
+DA:189,0
+DA:200,0
+DA:201,0
+DA:204,0
+DA:205,0
+DA:206,0
+DA:208,0
+DA:209,0
+DA:192,0
+DA:193,0
+DA:195,0
+DA:196,0
+DA:222,0
+DA:223,0
+DA:224,0
+DA:225,0
+DA:227,0
+DA:244,0
+DA:227,0
+DA:246,0
+DA:247,0
+DA:229,0
+DA:230,0
+DA:232,0
+LF:1146
+LH:478
+end_of_record
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/codecoverage/test/test_lcov_rewrite.py
@@ -0,0 +1,207 @@
+# 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 os
+import unittest
+from StringIO import StringIO
+
+from mozbuild.codecoverage import lcov_rewriter
+import mozunit
+
+here = os.path.dirname(__file__)
+
+basic_file = """TN:Compartment_5f7f5c30251800
+SF:resource://gre/modules/osfile.jsm
+FN:1,top-level
+FNDA:1,top-level
+FNF:1
+FNH:1
+BRDA:9,0,61,1
+BRF:1
+BRH:1
+DA:9,1
+DA:24,1
+LF:2
+LH:2
+end_of_record
+"""
+
+# These line numbers are (synthetically) sorted.
+multiple_records = """SF:resource://gre/modules/workers/require.js
+FN:1,top-level
+FN:80,.get
+FN:95,require
+FNDA:1,top-level
+FNF:3
+FNH:1
+BRDA:46,0,16,0
+BRDA:135,225,446,0
+BRF:2
+BRH:0
+DA:43,1
+DA:46,1
+DA:152,0
+DA:163,1
+LF:4
+LH:3
+end_of_record
+SF:resource://gre/modules/osfile/osfile_async_worker.js
+FN:12,top-level
+FN:30,worker.dispatch
+FN:34,worker.postMessage
+FN:387,do_close
+FN:392,exists
+FN:394,do_exists
+FN:400,unixSymLink
+FNDA:1,do_exists
+FNDA:1,exists
+FNDA:1,top-level
+FNDA:594,worker.dispatch
+FNF:7
+FNH:4
+BRDA:6,0,30,1
+BRDA:365,0,103,0
+BRF:2
+BRH:1
+DA:6,1
+DA:7,0
+DA:12,1
+DA:18,1
+DA:19,1
+DA:20,1
+DA:23,1
+DA:25,1
+DA:401,0
+DA:407,1
+LF:10
+LH:8
+end_of_record
+"""
+
+class TestLcovParser(unittest.TestCase):
+
+    def get_lcov(self, lcov_string):
+        fh = StringIO(lcov_string)
+        return lcov_rewriter.LcovFile(fh)
+
+    def parser_roundtrip(self, lcov_string, resummarize=False):
+        file_obj = self.get_lcov(lcov_string)
+        if resummarize:
+            for r in file_obj.records:
+                r.resummarize()
+        out = StringIO()
+        file_obj.print_file(out)
+        return out.getvalue()
+
+    def test_basic_parse(self):
+        output = self.parser_roundtrip(basic_file)
+        self.assertEqual(basic_file, output)
+
+        output = self.parser_roundtrip(multiple_records)
+        self.assertEqual(multiple_records, output)
+
+    def test_resummarize(self):
+        output = self.parser_roundtrip(basic_file, True)
+        self.assertEqual(basic_file, output)
+
+        output = self.parser_roundtrip(multiple_records, True)
+        self.assertEqual(multiple_records, output)
+
+
+multiple_included_files = """//@line 1 "foo.js"
+bazfoobar
+//@line 2 "bar.js"
+@foo@
+//@line 3 "foo.js"
+bazbarfoo
+//@line 2 "bar.js"
+foobarbaz
+//@line 3 "test.js"
+barfoobaz
+//@line 1 "baz.js"
+baz
+//@line 6 "f.js"
+fin
+"""
+
+class TestLineRemapping(unittest.TestCase):
+    def setUp(self):
+        self.lcov_rewriter = lcov_rewriter.LcovFileRewriter('', '', [])
+        self.pp_rewriter = self.lcov_rewriter.pp_rewriter
+
+    def get_lcov_file(self, name):
+        fpath = os.path.join(here, name)
+        with open(fpath) as fh:
+            lcov_file = lcov_rewriter.LcovFile(fh)
+        return lcov_file
+
+    def test_map_multiple_included(self):
+        self.pp_rewriter.populate_pp_info(StringIO(multiple_included_files),
+                                          '')
+        actual = self.pp_rewriter.pp_info['']
+        expected = {
+            (2, 3): ('foo.js', 1),
+            (4, 5): ('bar.js', 2),
+            (6, 7): ('foo.js', 3),
+            (8, 9): ('bar.js', 2),
+            (10, 11): ('test.js', 3),
+            (12, 13): ('baz.js', 1),
+            (14, 15): ('f.js', 6),
+        }
+
+        self.assertEqual(actual, expected)
+
+    def test_remap_lcov(self):
+        pp_remap = {
+            (1941, 2158): ('/home/chris/m-c/browser/base/content/newtab/dropPreview.js', 6),
+            (2159, 2331): ('/home/chris/m-c/browser/base/content/newtab/updater.js', 6),
+            (2584, 2674): ('/home/chris/m-c/browser/base/content/newtab/intro.js', 6),
+            (2332, 2443): ('/home/chris/m-c/browser/base/content/newtab/undo.js', 6),
+            (864, 985): ('/home/chris/m-c/browser/base/content/newtab/cells.js', 6),
+            (2444, 2454): ('/home/chris/m-c/browser/base/content/newtab/search.js', 6),
+            (1567, 1712): ('/home/chris/m-c/browser/base/content/newtab/drop.js', 6),
+            (2455, 2583): ('/home/chris/m-c/browser/base/content/newtab/customize.js', 6),
+            (1713, 1940): ('/home/chris/m-c/browser/base/content/newtab/dropTargetShim.js', 6),
+            (1402, 1548): ('/home/chris/m-c/browser/base/content/newtab/drag.js', 6),
+            (1549, 1566): ('/home/chris/m-c/browser/base/content/newtab/dragDataHelper.js', 6),
+            (453, 602): ('/home/chris/m-c/browser/base/content/newtab/page.js', 141),
+            (2675, 2678): ('/home/chris/m-c/browser/base/content/newtab/newTab.js', 70),
+            (56, 321): ('/home/chris/m-c/browser/base/content/newtab/transformations.js', 6),
+            (603, 863): ('/home/chris/m-c/browser/base/content/newtab/grid.js', 6),
+            (322, 452): ('/home/chris/m-c/browser/base/content/newtab/page.js', 6),
+            (986, 1401): ('/home/chris/m-c/browser/base/content/newtab/sites.js', 6)
+        }
+
+        lcov_file = self.get_lcov_file('sample_lcov.info')
+
+        self.assertEqual(len(lcov_file.records), 1)
+
+        # This summarization changes values due multiple reports per line coming
+        # from the JS engine (bug 1198356).
+        for r in lcov_file.records:
+            r.resummarize()
+            original_line_count = r.line_count
+            original_covered_line_count = r.covered_line_count
+            original_function_count = r.function_count
+            original_covered_function_count = r.covered_function_count
+
+        self.pp_rewriter.pp_info['/home/chris/m-c/browser/base/content/newtab/newTab.js'] = pp_remap
+        additions = []
+        for r in lcov_file.records:
+            additions += self.pp_rewriter.rewrite_record(r)
+        lcov_file.records = lcov_file.records + additions
+        self.assertEqual(len(lcov_file.records), 16)
+
+        # Lines/functions are only "moved" between records, not duplicated or omited.
+        self.assertEqual(original_line_count,
+                         sum(r.line_count for r in lcov_file.records))
+        self.assertEqual(original_covered_line_count,
+                         sum(r.covered_line_count for r in lcov_file.records))
+        self.assertEqual(original_function_count,
+                         sum(r.function_count for r in lcov_file.records))
+        self.assertEqual(original_covered_function_count,
+                         sum(r.covered_function_count for r in lcov_file.records))
+
+if __name__ == '__main__':
+    mozunit.main()