def make_html(modelA, modelB, f):
    sh = SourceHighlighter()

    # Approach: find peer files:
    sourcesA = modelA.get_source_files()
    sourcesB = modelB.get_source_files()

    #pprint(sourcesA)
    #pprint(sourcesB)
    # how to match?  givenpath may be good enough for our example
    sourcesA_by_internal_path = {}
    for fileA in sourcesA:
        sourcesA_by_internal_path[get_internal_filename(fileA)] = fileA
    sourcesB_by_internal_path = {}
    for fileB in sourcesB:
        sourcesB_by_internal_path[get_internal_filename(fileB)] = fileB
    internal_paths = set(sourcesA_by_internal_path.keys()
                         + sourcesB_by_internal_path.keys())

    sutA = list(modelA.iter_analyses())[0].metadata.sut
    sutB = list(modelB.iter_analyses())[0].metadata.sut

    title = '%s - comparison view' % sutA.name
    f.write('<html>\n')
    write_common_meta(f)
    f.write('<head><title>%s</title>\n' % title)

    f.write('    <style type="text/css">\n')

    write_common_css(f)

    f.write(sh.formatter.get_style_defs())

    f.write('      </style>\n')

    f.write('</head>\n')

    f.write('  <body>\n')

    generatorsA = modelA.get_generators()
    generatorsB = modelB.get_generators()
    generators = sorted(set(generatorsA + generatorsB))

    aisA_by_source_and_generator = modelA.get_analysis_issues_by_source_and_generator()
    aisB_by_source_and_generator = modelB.get_analysis_issues_by_source_and_generator()

    afsA_by_source = modelA.get_analysis_failures_by_source()
    afsB_by_source = modelB.get_analysis_failures_by_source()

    f.write('<p>Old build: <b>%s</b></p>' % sutA)
    f.write('<p>New build: <b>%s</b></p>' % sutB)

    f.write('    <table>\n')
    if 1:
        f.write('    <tr>\n')
        f.write('      <th>Old file</th>\n')
        f.write('      <th>New file</th>\n')
        for generator in generators:
            f.write('      <th>%s</th>\n' % generator.name)
        f.write('      <th>Notes</th>\n')
        f.write('    </tr>\n')
    for internal_path in sorted(internal_paths):
        fileA = sourcesA_by_internal_path.get(internal_path, None)
        fileB = sourcesB_by_internal_path.get(internal_path, None)
        f.write('    <tr>\n')
        if fileA:
            f.write('      <td><a href="#file-%s">%s</a></td>\n'
                    % (fileA.hash_.hexdigest, get_filename(fileA)))
        else:
            f.write('      <td></td>\n')
        if fileB:
            f.write('      <td><a href="#file-%s">%s</a></td>\n'
                    % (fileB.hash_.hexdigest, get_filename(fileB)))
        else:
            f.write('      <td></td>\n')
        for generator in generators:
            keyA = (fileA, generator)
            aisA = aisA_by_source_and_generator.get(keyA, set())
            keyB = (fileB, generator)
            aisB = aisB_by_source_and_generator.get(keyB, set())
            class_ = 'has_issues' if aisA or aisB else 'no_issues'
            f.write('      <td class="%s">%s / %s</td>\n' % (class_, len(aisA), len(aisB)))
        afsA = afsA_by_source.get(fileA, [])
        afsB = afsB_by_source.get(fileB, [])
        if afsA or afsB:
            f.write('      <td>Incomplete coverage: old has %i analysis failure(s), new has %i analysis failure(s)</td>\n'
                    % (len(afsA), len(afsB)))
        else:
            f.write('      <td></td>\n')

        f.write('    </tr>\n')
    f.write('    </table>\n')

    for internal_path in sorted(internal_paths):
        fileA = sourcesA_by_internal_path.get(internal_path, None)
        fileB = sourcesB_by_internal_path.get(internal_path, None)

        aisA = modelA.get_analysis_issues_by_source().get(fileA, set())
        aisB = modelB.get_analysis_issues_by_source().get(fileB, set())

        afsA = afsA_by_source.get(fileA, [])
        afsB = afsB_by_source.get(fileB, [])

        if fileA is not None:
            f.write('<a id="file-%s"/>' % fileA.hash_.hexdigest)
            if fileB is not None:
                f.write('<a id="file-%s"/>' % fileB.hash_.hexdigest)
                f.write('<h1>Comparison of old/new %s</h1>\n' % get_internal_filename(fileA))
            else:
                f.write('<h1>Removed file: %s</h1>\n' % get_internal_filename(fileA))
        else:
            assert fileB is not None
            f.write('<a id="file-%s"/>' % fileB.hash_.hexdigest)
            f.write('<h1>Added file: %s</h1>\n' % get_internal_filename(fileB))

        ci = ComparativeIssues(aisA, aisB)
        if ci.new:
            f.write('<h2>New issues</h2>')
            write_issue_table_for_file(f, fileB, ci.new)

        if ci.fixed:
            f.write('<h2>Fixed issues</h2>')
            write_issue_table_for_file(f, fileA, ci.fixed)

        if ci.inboth:
            f.write('<h2>Issues in both old/new</h2>')
            f.write('    <table>\n')
            f.write('    <tr>\n')
            f.write('      <th>Old location</th>\n')
            f.write('      <th>New location</th>\n')
            f.write('      <th>Tool</th>\n')
            f.write('      <th>Test ID</th>\n')
            f.write('      <th>Function</th>\n')
            f.write('      <th>Issue</th>\n')
            f.write('    </tr>\n')
            for aiA, aiB in sorted(ci.inboth,
                                   lambda ab1, ab2: AnalysisIssue.cmp(ab1[1], ab2[1])):
                f.write('    <tr>\n')
                f.write('      <td>%s:%i:%i</td>\n'
                        % (aiA.givenpath,
                           aiA.line,
                           aiA.column))
                f.write('      <td>%s:%i:%i</td>\n'
                        % (aiB.givenpath,
                           aiB.line,
                           aiB.column))
                f.write('      <td>%s</td>\n' % aiB.generator.name)
                f.write('      <td>%s</td>\n' % (aiB.testid if aiB.testid else ''))
                f.write('      <td>%s</td>\n' % (aiB.function.name if aiB.function else '')),
                f.write('      <td><a href="%s">%s</a></td>\n'
                        % ('#file-%s-line-%i' % (fileB.hash_.hexdigest, aiB.line),
                           aiB.message.text))
                f.write('    </tr>\n')
            f.write('    </table>\n')

        cf = ComparativeFailures(afsA, afsB)
        if cf.new:
            f.write('<h2>New failures</h2>')
            write_failure_table_for_file(f, fileB, cf.new)

        if cf.fixed:
            f.write('<h2>Fixed failures</h2>')
            write_failure_table_for_file(f, fileA, cf.fixed)

        if cf.inboth:
            f.write('<h2>Failures in both old/new</h2>')
            f.write('    <table>\n')
            f.write('    <tr>\n')
            f.write('      <th>Tool</th>\n')
            f.write('      <th>Failure ID</th>\n')
            f.write('      <th>Old location</th>\n')
            f.write('      <th>New location</th>\n')
            f.write('      <th>Function</th>\n')
            f.write('      <th>Message</th>\n')
            f.write('      <th>Data</th>\n')
            f.write('    </tr>\n')
            for afA, afB in sorted(cf.inboth,
                                   lambda ab1, ab2: AnalysisFailure.cmp(ab1[1], ab2[1])):
                f.write('    <tr>\n')
                f.write('      <td>%s</td>\n' % afB.generator.name)
                f.write('      <td>%s</td>\n' % afB.failureid)
                f.write('      <td>%s:%i:%i</td>\n'
                        % (afA.givenpath,
                           afA.line,
                           afA.column))
                f.write('      <td>%s:%i:%i</td>\n'
                        % (afB.givenpath,
                           afB.line,
                           afB.column))
                f.write('      <td>%s</td>\n' % (afB.function.name if afB.function else '')),
                f.write('      <td><a href="%s">%s</a></td>\n'
                        % ('#file-%s-line-%i' % (fileB.hash_.hexdigest, afB.line),
                           html_escape(str(afB.message))))
                f.write('      <td>%s</td>\n' % (html_escape(afB.customfields)) if afB.customfields else '')
                f.write('    </tr>\n')
            f.write('    </table>\n')

        write_html_diff(f, modelA, modelB, fileA, fileB, aisA, aisB, afsA, afsB, sh)

    f.write('  </body>\n')
    f.write('</html>\n')
def make_html(model, f):
    sh = SourceHighlighter()

    analyses = list(model.iter_analyses())

    title = ''
    f.write('<html>\n')
    write_common_meta(f)
    f.write('<head><title>%s</title>\n' % title)

    f.write('    <style type="text/css">\n')

    write_common_css(f)

    f.write(sh.formatter.get_style_defs())

    f.write('      </style>\n')

    f.write('</head>\n')

    f.write('  <body>\n')

    sources = model.get_source_files()
    generators = model.get_generators()
    ais_by_source = model.get_analysis_issues_by_source()
    ais_by_source_and_generator = model.get_analysis_issues_by_source_and_generator()
    afs_by_source = model.get_analysis_failures_by_source()

    f.write('    <table>\n')
    if 1:
        f.write('    <tr>\n')
        f.write('      <th>Source file</th>\n')
        for generator in generators:
            f.write('      <th>%s</th>\n' % generator.name)
        f.write('      <th>Notes</th>\n')
        f.write('    </tr>\n')
    for file_ in sources:
        f.write('    <tr>\n')
        f.write('      <td><a href="#file-%s">%s</a></td>\n'
                % (file_.hash_.hexdigest, get_filename(file_)))
        for generator in generators:
            key = (file_, generator)
            ais = ais_by_source_and_generator.get(key, set())
            class_ = 'has_issues' if ais else 'no_issues'
            f.write('      <td class="%s">%s</td>\n' % (class_, len(ais)))
        afs = afs_by_source.get(file_, [])
        if afs:
            f.write('      <td>Incomplete coverage: %i analysis failure(s)</td>\n'
                    % len(afs))
        else:
            f.write('      <td></td>\n')
        f.write('    </tr>\n')
    f.write('    </table>\n')

    for file_ in sources:
        f.write('<h2><a id="file-%s">%s</h2>\n' % (file_.hash_.hexdigest, get_filename(file_)))
        ais = ais_by_source.get(file_, set())
        if ais:
            write_issue_table_for_file(f, file_, ais)
        else:
            f.write('<p>No issues found</p>')
        afs = afs_by_source.get(file_, [])
        if afs:
            write_failure_table_for_file(f, file_, afs)
        # Include source inline:
        code = model.get_file_content(file_)

        # Write any lineless issues/failures at the start of the file:
        f.write('<a id="file-%s-line-0"/>' % (file_.hash_.hexdigest, ))
        for ai in ais:
            if ai.line is None:
                f.write(make_issue_note(ai))
        for af in afs:
            if af.line is None:
                f.write(make_failure_note(af))

        for i, line in enumerate(sh.highlight(code).splitlines()):
            f.write('<a id="file-%s-line-%i"/>' % (file_.hash_.hexdigest, i + 1))
            f.write(line)
            f.write('\n')
            for ai in ais:
                if ai.line == i + 1:
                    f.write(make_issue_note(ai))
            for af in afs:
                if af.line == i + 1:
                    f.write(make_failure_note(af))

    f.write('  </body>\n')
    f.write('</html>\n')